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

Compare commits

...

32 Commits

Author SHA1 Message Date
is-Xiaoen
30c8e77246 fix: improve type safety in httpWithTLS for custom RoundTripper (#861)
* fix: improve type safety in httpWithTLS for custom RoundTripper

Add type assertion check to handle cases where DefaultHTTPClient.Transport
is a custom http.RoundTripper implementation (not *http.Transport).

This improves upon the fix in PR #844 which only handled nil Transport.
The previous code would still panic if users set a custom RoundTripper:

  trans := baseTransport.(*http.Transport).Clone()  // panic if not *http.Transport

Now safely handles three scenarios:
1. Transport is nil -> use http.DefaultTransport
2. Transport is *http.Transport -> clone it
3. Transport is custom RoundTripper -> use http.DefaultTransport

Added comprehensive test cases:
- TestHttpWithTLS_NilTransport
- TestHttpWithTLS_CustomTransport
- TestHttpWithTLS_CustomRoundTripper

Related to #803

* refactor: reduce code duplication and complexity in httpWithTLS

- Eliminate duplicate http.DefaultTransport.Clone() calls
- Reduce cyclomatic complexity by simplifying conditional logic
- Use nil check pattern instead of nested else branches
- Maintain same functionality with cleaner code structure

This addresses golangci-lint warnings for dupl and gocyclo.

* fix: add newline at end of http_test.go

Fix gofmt -s compliance issue:
- File must end with newline character
- Addresses golangci-lint gofmt error on line 81

This fixes CI check failure.
2025-10-27 14:24:24 +08:00
mahongran
6f6e95cfdb SubscribeMsgSentList结构中的ErrorCode类型从int更新为string (#859) 2025-10-24 20:49:24 +08:00
silenceper
c806a0c172 Add star badge to README 2025-10-24 17:37:01 +08:00
zhangjiani
c136b878ce 调整企微回调URL参数tag,兼容kratos框架 (#855)
* fix: handle JSON parse error when API returns binary file instead of error JSON

* fix: add JSON tags to SignatureOptions struct fields for proper serialization

* fix: mod module

* fix: rollback

---------

Co-authored-by: tax <jia_deng@intsig.net>
2025-09-19 11:16:52 +08:00
zhangjiani
d4a81916d5 fix: handle JSON parse error when API returns binary file instead of error JSON (#852)
Co-authored-by: tax <jia_deng@intsig.net>
2025-09-14 19:47:00 +08:00
Outyua
ef1372b98a fix BatchGetExternalUserDetails to return NextCursor in response (#849) 2025-08-18 15:51:30 +08:00
silenceper
0d666b60ba update readme (#848)
* update readme

* update readme

* update readme
2025-07-29 23:31:01 +08:00
silenceper
e1122d42b0 Update FUNDING.yml 2025-07-29 23:13:42 +08:00
silenceper
be3f0d8bd5 Update FUNDING.yml 2025-07-29 22:52:50 +08:00
silenceper
66f9794d2f Update FUNDING.yml (#847) 2025-07-29 22:41:02 +08:00
silenceper
ee5f045b89 fix panic (#844) 2025-07-24 09:59:13 +08:00
silenceper
d35f0f0865 Ai dev (#846)
* add Claude Code workflow

* add Claude Code model
2025-07-23 23:43:46 +08:00
silenceper
bbad169706 add Claude Code workflow (#845) 2025-07-23 22:56:33 +08:00
ccfish
5927c26152 追加接口【微信物流服务 /微信物流服务(商家查看) /查询组件】 (#843)
* feat: 添加 微信物流服务 /微信物流服务(商家查看) /消息组件

* fix lint issues

* fix: 查询运单详情信息返回结果结构

* 追加官方文档地址

* feat: 追加接口【微信物流服务 /微信物流服务(商家查看) /查询组件】

* fix: comments

* fix: indents
2025-07-21 10:13:47 +08:00
markwang
8ebff5c29c feat: 微信小程序-运维中心 (#838)
* feat: 微信小程序-运维中心

* feat: 微信小程序-运维中心

* feat: 微信小程序-运维中心

* feat: 微信小程序-运维中心
2025-07-14 16:16:44 +08:00
ccfish
86ef690ecd feat: 添加 微信物流服务 /微信物流服务(商家查看) /消息组件 (#842) 2025-07-05 09:01:15 +08:00
markwang
ee85790123 微信小程序-数据分析-获取小程序性能数据 (#837)
* feat: 微信小程序-数据分析-性能数据

* feat: 微信小程序-数据分析-性能数据

* feat: 微信小程序-数据分析-性能数据
2025-07-01 19:14:44 +08:00
markwang
8a810837a4 feat: 微信小程序-动态消息及订阅消息 (#835)
* feat: 微信小程序-动态消息及订阅消息

* feat: 微信小程序-动态消息及订阅消息

* feat: 微信小程序-动态消息及订阅消息
2025-06-18 16:16:43 +08:00
yahuian
c51d41ee8a security 模块增加 context 调用函数 (#836) 2025-06-18 16:14:10 +08:00
Lien Li
24f812d187 feat: 支持Redis作为Cache的时候使用TLS (#834)
* feat: 支持Redis作为Cache的时候使用TLS

* feat: fix lint

* fix lint

* Update redis.go
2025-05-03 23:43:15 +08:00
markwang
dd43b7baa3 feat: 微信小程序-小程序链接 (#833)
* feat: 微信小程序-小程序链接

* feat: 微信小程序-小程序链接
2025-05-01 10:19:19 +08:00
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
35 changed files with 1972 additions and 117 deletions

4
.github/FUNDING.yml vendored
View File

@@ -1,8 +1,8 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
github: # silenceper
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
open_collective: gowechat
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry

38
.github/workflows/ai-dev.yaml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@beta
env:
ANTHROPIC_BASE_URL: "${{ secrets.ANTHROPIC_BASE_URL }}"
with:
model: "${{ secrets.ANTHROPIC_MODEL }}"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

View File

@@ -1,12 +1,13 @@
# WeChat SDK for Go
![Go](https://github.com/silenceper/wechat/workflows/Go/badge.svg?branch=release-2.0)
[![Go Report Card](https://goreportcard.com/badge/github.com/silenceper/wechat)](https://goreportcard.com/report/github.com/silenceper/wechat)
![Go](https://github.com/silenceper/wechat/actions/workflows/go.yml/badge.svg?branch=v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/silenceper/wechat/v2)](https://goreportcard.com/report/github.com/silenceper/wechat/v2)
[![pkg](https://img.shields.io/badge/dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/silenceper/wechat/v2?tab=doc)
![version](https://img.shields.io/badge/version-v2-green)
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/silenceper/wechat?sort=semver)
![star](https://gitcode.com/silenceper/wechat/star/badge.svg)
使用Golang开发的微信SDK简单、易用。
> 注意当前版本为v2版本v1版本已废弃
## 文档 && 例子
@@ -75,7 +76,13 @@ server.Send()
- 提交issue描述需要贡献的内容
- 完成更改后提交PR
## 公众号
## 感谢以下贡献者
<a href="https://opencollective.com/gowechat"><img src="https://opencollective.com/gowechat/contributors.svg?width=890" /></a>
## 作者公众号
![img](https://silenceper.oss-cn-beijing.aliyuncs.com/qrcode/search_study_program.png)

33
cache/redis.go vendored
View File

@@ -2,6 +2,8 @@ package cache
import (
"context"
"crypto/tls"
"net"
"time"
"github.com/go-redis/redis/v8"
@@ -15,25 +17,38 @@ type Redis struct {
// RedisOpts redis 连接属性
type RedisOpts struct {
Host string `yml:"host" json:"host"`
Username string `yaml:"username" json:"username"`
Password string `yml:"password" json:"password"`
Database int `yml:"database" json:"database"`
MaxIdle int `yml:"max_idle" json:"max_idle"`
MaxActive int `yml:"max_active" json:"max_active"`
IdleTimeout int `yml:"idle_timeout" json:"idle_timeout"` // second
Host string `json:"host" yml:"host"`
Username string `json:"username" yaml:"username"`
Password string `json:"password" yml:"password"`
Database int `json:"database" yml:"database"`
MaxIdle int `json:"max_idle" yml:"max_idle"`
MaxActive int `json:"max_active" yml:"max_active"`
IdleTimeout int `json:"idle_timeout" yml:"idle_timeout"` // second
UseTLS bool `json:"use_tls" yml:"use_tls"` // 是否使用TLS
}
// NewRedis 实例化
func NewRedis(ctx context.Context, opts *RedisOpts) *Redis {
conn := redis.NewUniversalClient(&redis.UniversalOptions{
uniOpt := &redis.UniversalOptions{
Addrs: []string{opts.Host},
DB: opts.Database,
Username: opts.Username,
Password: opts.Password,
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
MinIdleConns: opts.MaxIdle,
})
}
if opts.UseTLS {
h, _, err := net.SplitHostPort(opts.Host)
if err != nil {
h = opts.Host
}
uniOpt.TLSConfig = &tls.Config{
ServerName: h,
}
}
conn := redis.NewUniversalClient(uniOpt)
return &Redis{ctx: ctx, conn: conn}
}

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

@@ -30,6 +30,8 @@ const (
getAnalysisVisitDistributionURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitdistribution?access_token=%s"
// 访问页面
getAnalysisVisitPageURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitpage?access_token=%s"
// 获取小程序性能数据
getPerformanceDataURL = "https://api.weixin.qq.com/wxa/business/performance/boot?access_token=%s"
)
// Analysis analyis 数据分析
@@ -315,3 +317,67 @@ func (analysis *Analysis) GetAnalysisVisitPage(beginDate, endDate string) (resul
}
return
}
// GetPerformanceDataRequest 获取小程序性能数据请求
type GetPerformanceDataRequest struct {
Module string `json:"module"`
Time PerformanceDataTime `json:"time"`
Params []PerformanceDataParams `json:"params"`
}
// PerformanceDataTime 获取小程序性能数据开始和结束日期
type PerformanceDataTime struct {
BeginTimestamp int64 `json:"begin_timestamp"`
EndTimestamp int64 `json:"end_timestamp"`
}
// PerformanceDataParams 获取小程序性能数据查询条件
type PerformanceDataParams struct {
Field string `json:"field"`
Value string `json:"value"`
}
// GetPerformanceDataResponse 获取小程序性能数据响应
type GetPerformanceDataResponse struct {
util.CommonError
Body PerformanceDataBody `json:"body"`
}
// PerformanceDataBody 性能数据
type PerformanceDataBody struct {
Tables []PerformanceDataTable `json:"tables"`
Count int64 `json:"count"`
}
// PerformanceDataTable 数据数组
type PerformanceDataTable struct {
ID string `json:"id"`
Lines []PerformanceDataTableLine `json:"lines"`
Zh string `json:"zh"`
}
// PerformanceDataTableLine 按时间排列的性能数据
type PerformanceDataTableLine struct {
Fields []PerformanceDataTableLineField `json:"fields"`
}
// PerformanceDataTableLineField 单天的性能数据
type PerformanceDataTableLineField struct {
RefDate string `json:"refdate"`
Value string `json:"value"`
}
// GetPerformanceData 获取小程序性能数据
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/data-analysis/others/getPerformanceData.html
func (analysis *Analysis) GetPerformanceData(req *GetPerformanceDataRequest) (res GetPerformanceDataResponse, err error) {
var accessToken string
if accessToken, err = analysis.GetAccessToken(); err != nil {
return
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(getPerformanceDataURL, accessToken), req); err != nil {
return
}
err = util.DecodeWithError(response, &res, "GetPerformanceData")
return
}

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

@@ -0,0 +1,295 @@
package express
import (
"context"
"fmt"
"github.com/silenceper/wechat/v2/util"
)
const (
// 传运单接口,商户使用此接口向微信提供某交易单号对应的运单号。微信后台会跟踪运单的状态变化
openMsgTraceWaybillURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/trace_waybill?access_token=%s"
// 查询运单接口商户在调用完trace_waybill接口后可以使用本接口查询到对应运单的详情信息
openMsgQueryTraceURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/query_trace?access_token=%s"
// 更新物流信息,更新物品信息
openMsgUpdateWaybillGoodsURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/update_waybill_goods?access_token=%s"
// 传运单接口,商户使用此接口向微信提供某交易单号对应的运单号。微信后台会跟踪运单的状态变化,在关键物流节点给下单用户推送消息通知
openMsgFollowWaybillURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/follow_waybill?access_token=%s"
// 查运单接口商户在调用完follow_waybill接口后可以使用本接口查询到对应运单的详情信息
openMsgQueryFollowTraceURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/query_follow_trace?access_token=%s"
// 更新物品信息接口
openMsgUpdateFollowWaybillGoodsURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/update_follow_waybill_goods?access_token=%s"
// 获取运力id列表
openMsgGetDeliveryListURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/get_delivery_list?access_token=%s"
)
// TraceWaybill 传运单
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_search.html#_2-%E6%8E%A5%E5%8F%A3%E5%88%97%E8%A1%A8
func (express *Express) TraceWaybill(ctx context.Context, in *TraceWaybillRequest) (res TraceWaybillResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(openMsgTraceWaybillURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, in)
if err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &res, "TraceWaybill")
return
}
// QueryTrace 查询运单详情信息
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_search.html#_2-%E6%8E%A5%E5%8F%A3%E5%88%97%E8%A1%A8
func (express *Express) QueryTrace(ctx context.Context, in *QueryTraceRequest) (res QueryTraceResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(openMsgQueryTraceURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, in)
if err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &res, "QueryTrace")
return
}
// UpdateWaybillGoods 更新物品信息
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_search.html#_2-%E6%8E%A5%E5%8F%A3%E5%88%97%E8%A1%A8
func (express *Express) UpdateWaybillGoods(ctx context.Context, in *UpdateWaybillGoodsRequest) (err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(openMsgUpdateWaybillGoodsURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, in)
if err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithCommonError(response, "UpdateWaybillGoods")
return
}
// FollowWaybill 传运单
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_open_msg.html#_4-1%E3%80%81%E4%BC%A0%E8%BF%90%E5%8D%95%E6%8E%A5%E5%8F%A3-follow-waybill
func (express *Express) FollowWaybill(ctx context.Context, in *FollowWaybillRequest) (res FollowWaybillResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(openMsgFollowWaybillURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, in)
if err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &res, "FollowWaybill")
return
}
// QueryFollowTrace 查询运单详情信息
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_open_msg.html#_4-2%E3%80%81%E6%9F%A5%E8%BF%90%E5%8D%95%E6%8E%A5%E5%8F%A3-query-follow-trace
func (express *Express) QueryFollowTrace(ctx context.Context, in *QueryFollowTraceRequest) (res QueryFollowTraceResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(openMsgQueryFollowTraceURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, in)
if err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &res, "QueryFollowTrace")
return
}
// UpdateFollowWaybillGoods 更新物品信息
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_open_msg.html#_4-3%E3%80%81%E6%9B%B4%E6%96%B0%E7%89%A9%E5%93%81%E4%BF%A1%E6%81%AF%E6%8E%A5%E5%8F%A3-update-follow-waybill-goods
func (express *Express) UpdateFollowWaybillGoods(ctx context.Context, in *UpdateFollowWaybillGoodsRequest) (err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(openMsgUpdateFollowWaybillGoodsURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, in)
if err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithCommonError(response, "UpdateFollowWaybillGoods")
return
}
// GetDeliveryList 获取运力id列表
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_open_msg.html#_4-4%E8%8E%B7%E5%8F%96%E8%BF%90%E5%8A%9Bid%E5%88%97%E8%A1%A8get-delivery-list
func (express *Express) GetDeliveryList(ctx context.Context) (res GetDeliveryListResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(openMsgGetDeliveryListURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, map[string]interface{}{})
if err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &res, "GetDeliveryList")
return
}
// TraceWaybillRequest 传运单接口请求参数
type TraceWaybillRequest struct {
GoodsInfo FollowWaybillGoodsInfo `json:"goods_info"` // 必选,商品信息
Openid string `json:"openid"` // 必选用户openid
SenderPhone string `json:"sender_phone"` // 寄件人手机号
ReceiverPhone string `json:"receiver_phone"` // 必选,收件人手机号,部分运力需要用户手机号作为查单依据
DeliveryID string `json:"delivery_id"` // 运力id运单号所属运力公司id
WaybillID string `json:"waybill_id"` // 必选,运单号
TransID string `json:"trans_id"` // 必选交易单号微信支付生成的交易单号一般以420开头
OrderDetailPath string `json:"order_detail_path"` // 订单详情页地址
}
// TraceWaybillResponse 传运单接口返回参数
type TraceWaybillResponse struct {
util.CommonError
WaybillToken string `json:"waybill_token"` // 查询id
}
// QueryTraceRequest 查询运单详情接口请求参数
type QueryTraceRequest struct {
WaybillToken string `json:"waybill_token"` // 必选查询id
}
// QueryTraceResponse 查询运单详情接口返回参数
type QueryTraceResponse struct {
util.CommonError
WaybillInfo FlowWaybillInfo `json:"waybill_info"` // 运单信息
ShopInfo FollowWaybillShopInfo `json:"shop_info"` // 商品信息
DeliveryInfo FlowWaybillDeliveryInfo `json:"delivery_info"` // 运力信息
}
// UpdateWaybillGoodsRequest 更新物品信息接口请求参数
type UpdateWaybillGoodsRequest struct {
WaybillToken string `json:"waybill_token"` // 必选查询id
GoodsInfo FollowWaybillGoodsInfo `json:"goods_info"` // 必选,商品信息
}
// FollowWaybillRequest 传运单接口请求参数
type FollowWaybillRequest struct {
GoodsInfo FollowWaybillGoodsInfo `json:"goods_info"` // 必选,商品信息
Openid string `json:"openid"` // 必选用户openid
SenderPhone string `json:"sender_phone"` // 寄件人手机号
ReceiverPhone string `json:"receiver_phone"` // 必选,收件人手机号,部分运力需要用户手机号作为查单依据
DeliveryID string `json:"delivery_id"` // 运力id运单号所属运力公司id
WaybillID string `json:"waybill_id"` // 必选,运单号
TransID string `json:"trans_id"` // 必选交易单号微信支付生成的交易单号一般以420开头
OrderDetailPath string `json:"order_detail_path"` // 订单详情页地址
}
// FollowWaybillGoodsInfo 商品信息
type FollowWaybillGoodsInfo struct {
DetailList []FollowWaybillGoodsInfoItem `json:"detail_list"`
}
// FollowWaybillShopInfo 商品信息
type FollowWaybillShopInfo struct {
GoodsInfo FollowWaybillGoodsInfo `json:"goods_info"` // 商品信息
}
// FollowWaybillGoodsInfoItem 商品信息详情
type FollowWaybillGoodsInfoItem struct {
GoodsName string `json:"goods_name"` // 必选,商品名称(最大长度为utf-8编码下的60个字符
GoodsImgURL string `json:"goods_img_url"` // 必选商品图片url
GoodsDesc string `json:"goods_desc,omitempty"` // 商品详情描述不传默认取“商品名称”值最多40汉字
}
// FollowWaybillResponse 传运单接口返回参数
type FollowWaybillResponse struct {
util.CommonError
WaybillToken string `json:"waybill_token"` // 查询id
}
// QueryFollowTraceRequest 查询运单详情信息请求参数
type QueryFollowTraceRequest struct {
WaybillToken string `json:"waybill_token"` // 必选查询id
}
// QueryFollowTraceResponse 查询运单详情信息返回参数
type QueryFollowTraceResponse struct {
util.CommonError
WaybillInfo FlowWaybillInfo `json:"waybill_info"` // 运单信息
ShopInfo FollowWaybillShopInfo `json:"shop_info"` // 商品信息
DeliveryInfo FlowWaybillDeliveryInfo `json:"delivery_info"` // 运力信息
}
// FlowWaybillInfo 运单信息
type FlowWaybillInfo struct {
WaybillID string `json:"waybill_id"` // 运单号
Status WaybillStatus `json:"status"` // 运单状态
}
// UpdateFollowWaybillGoodsRequest 修改运单商品信息请求参数
type UpdateFollowWaybillGoodsRequest struct {
WaybillToken string `json:"waybill_token"` // 必选查询id
GoodsInfo FollowWaybillGoodsInfo `json:"goods_info"` // 必选,商品信息
}
// GetDeliveryListResponse 获取运力id列表返回参数
type GetDeliveryListResponse struct {
util.CommonError
DeliveryList []FlowWaybillDeliveryInfo `json:"delivery_list"` // 运力公司列表
Count int `json:"count"` // 运力公司个数
}
// FlowWaybillDeliveryInfo 运力公司信息
type FlowWaybillDeliveryInfo struct {
DeliveryID string `json:"delivery_id"` // 运力公司id
DeliveryName string `json:"delivery_name"` // 运力公司名称
}
// WaybillStatus 运单状态
type WaybillStatus int
const (
// WaybillStatusNotExist 运单不存在或者未揽收
WaybillStatusNotExist WaybillStatus = iota
// WaybillStatusPicked 已揽件
WaybillStatusPicked
// WaybillStatusTransporting 运输中
WaybillStatusTransporting
// WaybillStatusDispatching 派件中
WaybillStatusDispatching
// WaybillStatusSigned 已签收
WaybillStatusSigned
// WaybillStatusException 异常
WaybillStatusException
// WaybillStatusSignedByOthers 代签收
WaybillStatusSignedByOthers
)

View File

@@ -0,0 +1,16 @@
package express
import (
"github.com/silenceper/wechat/v2/miniprogram/context"
)
// Express 微信物流服务
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/introduction.html
type Express struct {
*context.Context
}
// NewExpress init
func NewExpress(ctx *context.Context) *Express {
return &Express{ctx}
}

View File

@@ -556,7 +556,7 @@ type SubscribeMsgSentEvent struct {
type SubscribeMsgSentList struct {
TemplateID string `xml:"TemplateId" json:"TemplateId"`
MsgID string `xml:"MsgID" json:"MsgID"`
ErrorCode int `xml:"ErrorCode" json:"ErrorCode"`
ErrorCode string `xml:"ErrorCode" json:"ErrorCode"`
ErrorStatus string `xml:"ErrorStatus" json:"ErrorStatus"`
}

View File

@@ -8,10 +8,12 @@ import (
)
const (
// createActivityURL 创建activity_id
createActivityURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/activityid/create?access_token=%s"
// createActivityIDURL 创建activity_id
createActivityIDURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/activityid/create?access_token=%s&unionid=%s&openid=%s"
// SendUpdatableMsgURL 修改动态消息
setUpdatableMsgURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/updatablemsg/send?access_token=%s"
// setChatToolMsgURL 修改小程序聊天工具的动态卡片消息
setChatToolMsgURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/chattoolmsg/send?access_token=%s"
)
// UpdatableTargetState 动态消息状态
@@ -38,15 +40,26 @@ func NewUpdatableMessage(ctx *context.Context) *UpdatableMessage {
}
}
// CreateActivityIDRequest 创建activity_id请求
type CreateActivityIDRequest struct {
UnionID string
OpenID string
}
// CreateActivityID 创建activity_id
func (updatableMessage *UpdatableMessage) CreateActivityID() (res CreateActivityIDResponse, err error) {
func (updatableMessage *UpdatableMessage) CreateActivityID() (CreateActivityIDResponse, error) {
return updatableMessage.CreateActivityIDWithReq(&CreateActivityIDRequest{})
}
// CreateActivityIDWithReq 创建activity_id
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/updatable-message/createActivityId.html
func (updatableMessage *UpdatableMessage) CreateActivityIDWithReq(req *CreateActivityIDRequest) (res CreateActivityIDResponse, err error) {
accessToken, err := updatableMessage.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(createActivityURL, accessToken)
response, err := util.HTTPGet(uri)
url := fmt.Sprintf(createActivityIDURL, accessToken, req.UnionID, req.OpenID)
response, err := util.HTTPGet(url)
if err != nil {
return
}
@@ -100,3 +113,35 @@ type SendUpdatableMsgReq struct {
TemplateInfo UpdatableMsgTemplate `json:"template_info"`
TargetState UpdatableTargetState `json:"target_state"`
}
// SetChatToolMsgRequest 修改小程序聊天工具的动态卡片消息请求
type SetChatToolMsgRequest struct {
VersionType int64 `json:"version_type"`
TargetState UpdatableTargetState `json:"target_state"`
ActivityID string `json:"activity_id"`
TemplateID string `json:"template_id"`
ParticipatorInfoList []ParticipatorInfo `json:"participator_info_list,omitempty"`
}
// ParticipatorInfo 更新后的聊天室成员状态
type ParticipatorInfo struct {
State int `json:"state"`
GroupOpenID string `json:"group_openid"`
}
// SetChatToolMsg 修改小程序聊天工具的动态卡片消息
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/updatable-message/setChatToolMsg.html
func (updatableMessage *UpdatableMessage) SetChatToolMsg(req *SetChatToolMsgRequest) error {
var (
accessToken string
err error
)
if accessToken, err = updatableMessage.GetAccessToken(); err != nil {
return err
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(setChatToolMsgURL, accessToken), req); err != nil {
return err
}
return util.DecodeWithCommonError(response, "SetChatToolMsg")
}

View File

@@ -10,8 +10,10 @@ import (
"github.com/silenceper/wechat/v2/miniprogram/content"
"github.com/silenceper/wechat/v2/miniprogram/context"
"github.com/silenceper/wechat/v2/miniprogram/encryptor"
"github.com/silenceper/wechat/v2/miniprogram/express"
"github.com/silenceper/wechat/v2/miniprogram/message"
"github.com/silenceper/wechat/v2/miniprogram/minidrama"
"github.com/silenceper/wechat/v2/miniprogram/operation"
"github.com/silenceper/wechat/v2/miniprogram/order"
"github.com/silenceper/wechat/v2/miniprogram/privacy"
"github.com/silenceper/wechat/v2/miniprogram/qrcode"
@@ -179,3 +181,13 @@ func (miniProgram *MiniProgram) GetRedPacketCover() *redpacketcover.RedPacketCov
func (miniProgram *MiniProgram) GetUpdatableMessage() *message.UpdatableMessage {
return message.NewUpdatableMessage(miniProgram.ctx)
}
// GetOperation 小程序运维中心
func (miniProgram *MiniProgram) GetOperation() *operation.Operation {
return operation.NewOperation(miniProgram.ctx)
}
// GetExpress 微信物流服务
func (miniProgram *MiniProgram) GetExpress() *express.Express {
return express.NewExpress(miniProgram.ctx)
}

View File

@@ -0,0 +1,456 @@
package operation
import (
"fmt"
"github.com/silenceper/wechat/v2/miniprogram/context"
"github.com/silenceper/wechat/v2/util"
)
const (
// getDomainInfoURL 查询域名配置
getDomainInfoURL = "https://api.weixin.qq.com/wxa/getwxadevinfo?access_token=%s"
// getPerformanceURL 获取性能数据
getPerformanceURL = "https://api.weixin.qq.com/wxaapi/log/get_performance?access_token=%s"
// getSceneListURL 获取访问来源
getSceneListURL = "https://api.weixin.qq.com/wxaapi/log/get_scene?access_token=%s"
// getVersionListURL 获取客户端版本
getVersionListURL = "https://api.weixin.qq.com/wxaapi/log/get_client_version?access_token=%s"
// realTimeLogSearchURL 查询实时日志
realTimeLogSearchURL = "https://api.weixin.qq.com/wxaapi/userlog/userlog_search?%s"
// getFeedbackListURL 获取用户反馈列表
getFeedbackListURL = "https://api.weixin.qq.com/wxaapi/feedback/list?%s"
// getJsErrDetailURL 查询js错误详情
getJsErrDetailURL = "https://api.weixin.qq.com/wxaapi/log/jserr_detail?access_token=%s"
// getJsErrListURL 查询错误列表
getJsErrListURL = "https://api.weixin.qq.com/wxaapi/log/jserr_list?access_token=%s"
// getGrayReleasePlanURL 获取分阶段发布详情
getGrayReleasePlanURL = "https://api.weixin.qq.com/wxa/getgrayreleaseplan?access_token=%s"
)
// Operation 运维中心
type Operation struct {
*context.Context
}
// NewOperation 实例化
func NewOperation(ctx *context.Context) *Operation {
return &Operation{ctx}
}
// GetDomainInfoRequest 查询域名配置请求
type GetDomainInfoRequest struct {
Action string `json:"action"`
}
// GetDomainInfoResponse 查询域名配置响应
type GetDomainInfoResponse struct {
util.CommonError
RequestDomain []string `json:"requestdomain"`
WsRequestDomain []string `json:"wsrequestdomain"`
UploadDomain []string `json:"uploaddomain"`
DownloadDomain []string `json:"downloaddomain"`
UDPDomain []string `json:"udpdomain"`
BizDomain []string `json:"bizdomain"`
}
// GetDomainInfo 查询域名配置
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getDomainInfo.html
func (o *Operation) GetDomainInfo(req *GetDomainInfoRequest) (res GetDomainInfoResponse, err error) {
var accessToken string
if accessToken, err = o.GetAccessToken(); err != nil {
return
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(getDomainInfoURL, accessToken), req); err != nil {
return
}
err = util.DecodeWithError(response, &res, "GetDomainInfo")
return
}
// GetPerformanceRequest 获取性能数据请求
type GetPerformanceRequest struct {
CostTimeType int64 `json:"cost_time_type"`
DefaultStartTime int64 `json:"default_start_time"`
DefaultEndTime int64 `json:"default_end_time"`
Device string `json:"device"`
IsDownloadCode string `json:"is_download_code"`
Scene string `json:"scene"`
NetworkType string `json:"networktype"`
}
// GetPerformanceResponse 获取性能数据响应
type GetPerformanceResponse struct {
util.CommonError
DefaultTimeData string `json:"default_time_data"`
CompareTimeData string `json:"compare_time_data"`
}
// PerformanceDefaultTimeData 查询数据
type PerformanceDefaultTimeData struct {
List []DefaultTimeDataItem `json:"list"`
}
// DefaultTimeDataItem 查询数据
type DefaultTimeDataItem struct {
RefData string `json:"ref_data"`
CostTimeType int64 `json:"cost_time_type"`
CostTime int64 `json:"cost_time"`
}
// GetPerformance 获取性能数据
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getPerformance.html
func (o *Operation) GetPerformance(req *GetPerformanceRequest) (res GetPerformanceResponse, err error) {
var accessToken string
if accessToken, err = o.GetAccessToken(); err != nil {
return
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(getPerformanceURL, accessToken), req); err != nil {
return
}
err = util.DecodeWithError(response, &res, "GetPerformance")
return
}
// GetSceneListResponse 获取访问来源响应
type GetSceneListResponse struct {
util.CommonError
Scene []Scene `json:"scene"`
}
// Scene 访问来源
type Scene struct {
Name string `json:"name"`
Value string `json:"value"`
}
// GetSceneList 获取访问来源
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getSceneList.html
func (o *Operation) GetSceneList() (res GetSceneListResponse, err error) {
var accessToken string
if accessToken, err = o.GetAccessToken(); err != nil {
return
}
var response []byte
if response, err = util.HTTPGet(fmt.Sprintf(getSceneListURL, accessToken)); err != nil {
return
}
err = util.DecodeWithError(response, &res, "GetSceneList")
return
}
// GetVersionListResponse 获取客户端版本响应
type GetVersionListResponse struct {
util.CommonError
CvList []ClientVersion `json:"cvlist"`
}
// ClientVersion 客户端版本
type ClientVersion struct {
Type int64 `json:"type"`
ClientVersionList []string `json:"client_version_list"`
}
// GetVersionList 获取客户端版本
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getVersionList.html
func (o *Operation) GetVersionList() (res GetVersionListResponse, err error) {
var accessToken string
if accessToken, err = o.GetAccessToken(); err != nil {
return
}
var response []byte
if response, err = util.HTTPGet(fmt.Sprintf(getVersionListURL, accessToken)); err != nil {
return
}
err = util.DecodeWithError(response, &res, "GetVersionList")
return
}
// RealTimeLogSearchRequest 查询实时日志请求
type RealTimeLogSearchRequest struct {
Date string
BeginTime int64
EndTime int64
Start int64
Limit int64
Level int64
TraceID string
URL string
ID string
FilterMsg string
}
// RealTimeLogSearchResponse 查询实时日志响应
type RealTimeLogSearchResponse struct {
util.CommonError
Data RealTimeLogSearchData `json:"data"`
}
// RealTimeLogSearchData 日志数据和日志条数总量
type RealTimeLogSearchData struct {
List []RealTimeLogSearchDataList `json:"list"`
Total int64 `json:"total"`
}
// RealTimeLogSearchDataList 日志数据列表
type RealTimeLogSearchDataList struct {
Level int64 `json:"level"`
LibraryVersion string `json:"libraryVersion"`
ClientVersion string `json:"clientVersion"`
ID string `json:"id"`
Timestamp int64 `json:"timestamp"`
Platform int64 `json:"platform"`
URL string `json:"url"`
TraceID string `json:"traceid"`
FilterMsg string `json:"filterMsg"`
Msg []RealTimeLogSearchDataListMsg `json:"msg"`
}
// RealTimeLogSearchDataListMsg 日志内容数组
type RealTimeLogSearchDataListMsg struct {
Time int64 `json:"time"`
Level int64 `json:"level"`
Msg []string `json:"msg"`
}
// RealTimeLogSearch 查询实时日志
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/realtimelogSearch.html
func (o *Operation) RealTimeLogSearch(req *RealTimeLogSearchRequest) (res RealTimeLogSearchResponse, err error) {
var accessToken string
if accessToken, err = o.GetAccessToken(); err != nil {
return
}
params := map[string]interface{}{
"access_token": accessToken,
"date": req.Date,
"begintime": req.BeginTime,
"endtime": req.EndTime,
}
if req.Start > 0 {
params["start"] = req.Start
}
if req.Limit > 0 {
params["limit"] = req.Limit
}
if req.TraceID != "" {
params["traceId"] = req.TraceID
}
if req.URL != "" {
params["url"] = req.URL
}
if req.ID != "" {
params["id"] = req.ID
}
if req.FilterMsg != "" {
params["filterMsg"] = req.FilterMsg
}
if req.Level > 0 {
params["level"] = req.Level
}
query := util.Query(params)
var response []byte
if response, err = util.HTTPGet(fmt.Sprintf(realTimeLogSearchURL, query)); err != nil {
return
}
err = util.DecodeWithError(response, &res, "RealTimeLogSearch")
return
}
// GetFeedbackListRequest 获取用户反馈列表请求
type GetFeedbackListRequest struct {
Page int64
Num int64
Type int64
}
// GetFeedbackListResponse 获取用户反馈列表响应
type GetFeedbackListResponse struct {
util.CommonError
TotalNum int64 `json:"total_num"`
List []Feedback `json:"list"`
}
// Feedback 反馈列表
type Feedback struct {
RecordID int64 `json:"record_id"`
CreateTime int64 `json:"create_time"`
Content string `json:"content"`
Phone string `json:"phone"`
OpenID string `json:"openid"`
Nickname string `json:"nickname"`
HeadURL string `json:"head_url"`
Type int64 `json:"type"`
MediaIDS []string `json:"mediaIds"`
SystemInfo string `json:"systemInfo"`
}
// GetFeedbackList 获取用户反馈列表
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getFeedback.html
func (o *Operation) GetFeedbackList(req *GetFeedbackListRequest) (res GetFeedbackListResponse, err error) {
var accessToken string
if accessToken, err = o.GetAccessToken(); err != nil {
return
}
params := map[string]interface{}{
"access_token": accessToken,
"page": req.Page,
"num": req.Num,
}
if req.Type > 0 {
params["type"] = req.Type
}
query := util.Query(params)
var response []byte
if response, err = util.HTTPGet(fmt.Sprintf(getFeedbackListURL, query)); err != nil {
return
}
err = util.DecodeWithError(response, &res, "GetFeedbackList")
return
}
// GetJsErrDetailRequest 查询js错误详情请求
type GetJsErrDetailRequest struct {
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
ErrorMsgMd5 string `json:"errorMsgMd5"`
ErrorStackMd5 string `json:"errorStackMd5"`
AppVersion string `json:"appVersion"`
SdkVersion string `json:"sdkVersion"`
OsName string `json:"osName"`
ClientVersion string `json:"clientVersion"`
OpenID string `json:"openid"`
Offset int64 `json:"offset"`
Limit int64 `json:"limit"`
Desc string `json:"desc"`
}
// GetJsErrDetailResponse 查询js错误详情响应
type GetJsErrDetailResponse struct {
util.CommonError
TotalCount int64 `json:"totalCount"`
OpenID string `json:"openid"`
Data []JsErrDetailData `json:"data"`
}
// JsErrDetailData 错误列表
type JsErrDetailData struct {
Count string `json:"Count"`
SdkVersion string `json:"sdkVersion"`
ClientVersion string `json:"ClientVersion"`
ErrorStackMd5 string `json:"errorStackMd5"`
TimeStamp string `json:"TimeStamp"`
AppVersion string `json:"appVersion"`
ErrorMsgMd5 string `json:"errorMsgMd5"`
ErrorMsg string `json:"errorMsg"`
ErrorStack string `json:"errorStack"`
Ds string `json:"Ds"`
OsName string `json:"OsName"`
OpenID string `json:"openId"`
PluginVersion string `json:"pluginversion"`
AppID string `json:"appId"`
DeviceModel string `json:"DeviceModel"`
Source string `json:"source"`
Route string `json:"route"`
Uin string `json:"Uin"`
Nickname string `json:"nickname"`
}
// GetJsErrDetail 查询js错误详情
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getJsErrDetail.html
func (o *Operation) GetJsErrDetail(req *GetJsErrDetailRequest) (res GetJsErrDetailResponse, err error) {
var accessToken string
if accessToken, err = o.GetAccessToken(); err != nil {
return
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(getJsErrDetailURL, accessToken), req); err != nil {
return
}
err = util.DecodeWithError(response, &res, "GetJsErrDetail")
return
}
// GetJsErrListRequest 查询错误列表请求
type GetJsErrListRequest struct {
AppVersion string `json:"appVersion"`
ErrType string `json:"errType"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
Keyword string `json:"keyword"`
OpenID string `json:"openid"`
OrderBy string `json:"orderby"`
Desc string `json:"desc"`
Offset int64 `json:"offset"`
Limit int64 `json:"limit"`
}
// GetJsErrListResponse 查询错误列表响应
type GetJsErrListResponse struct {
util.CommonError
TotalCount int64 `json:"totalCount"`
OpenID string `json:"openid"`
Data []JsErrListData `json:"data"`
}
// JsErrListData 错误列表
type JsErrListData struct {
ErrorMsgMd5 string `json:"errorMsgMd5"`
ErrorMsg string `json:"errorMsg"`
Uv int64 `json:"uv"`
Pv int64 `json:"pv"`
ErrorStackMd5 string `json:"errorStackMd5"`
ErrorStack string `json:"errorStack"`
PvPercent string `json:"pvPercent"`
UvPercent string `json:"uvPercent"`
}
// GetJsErrList 查询错误列表
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getJsErrList.html
func (o *Operation) GetJsErrList(req *GetJsErrListRequest) (res GetJsErrListResponse, err error) {
var accessToken string
if accessToken, err = o.GetAccessToken(); err != nil {
return
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(getJsErrListURL, accessToken), req); err != nil {
return
}
err = util.DecodeWithError(response, &res, "GetJsErrList")
return
}
// GetGrayReleasePlanResponse 获取分阶段发布详情响应
type GetGrayReleasePlanResponse struct {
util.CommonError
GrayReleasePlan GrayReleasePlanDetail `json:"gray_release_plan"`
}
// GrayReleasePlanDetail 分阶段发布计划详情
type GrayReleasePlanDetail struct {
Status int64 `json:"status"`
CreateTimestamp int64 `json:"create_timestamp"`
GrayPercentage int64 `json:"gray_percentage"`
SupportExperiencerFirst bool `json:"support_experiencer_first"`
SupportDebugerFirst bool `json:"support_debuger_first"`
}
// GetGrayReleasePlan 获取分阶段发布详情
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getGrayReleasePlan.html
func (o *Operation) GetGrayReleasePlan() (res GetGrayReleasePlanResponse, err error) {
var accessToken string
if accessToken, err = o.GetAccessToken(); err != nil {
return
}
var response []byte
if response, err = util.HTTPGet(fmt.Sprintf(getGrayReleasePlanURL, accessToken)); err != nil {
return
}
err = util.DecodeWithError(response, &res, "GetGrayReleasePlan")
return
}

View File

@@ -1,6 +1,7 @@
package security
import (
context2 "context"
"fmt"
"strconv"
@@ -64,7 +65,12 @@ type MediaCheckAsyncRequest struct {
// MediaCheckAsync 异步校验图片/音频是否含有违法违规内容
func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID string, err error) {
accessToken, err := security.GetAccessToken()
return security.MediaCheckAsyncContext(context2.Background(), in)
}
// MediaCheckAsyncContext 异步校验图片/音频是否含有违法违规内容
func (security *Security) MediaCheckAsyncContext(ctx context2.Context, in *MediaCheckAsyncRequest) (traceID string, err error) {
accessToken, err := security.GetAccessTokenContext(ctx)
if err != nil {
return
}
@@ -77,7 +83,7 @@ func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID s
req.Version = 2
uri := fmt.Sprintf(mediaCheckAsyncURL, accessToken)
response, err := util.PostJSON(uri, req)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return
}
@@ -222,7 +228,12 @@ func (security *Security) MsgCheckV1(content string) (res MsgCheckResponse, err
// MsgCheck 检查一段文本是否含有违法违规内容
func (security *Security) MsgCheck(in *MsgCheckRequest) (res MsgCheckResponse, err error) {
accessToken, err := security.GetAccessToken()
return security.MsgCheckContext(context2.Background(), in)
}
// MsgCheckContext 检查一段文本是否含有违法违规内容
func (security *Security) MsgCheckContext(ctx context2.Context, in *MsgCheckRequest) (res MsgCheckResponse, err error) {
accessToken, err := security.GetAccessTokenContext(ctx)
if err != nil {
return
}
@@ -235,7 +246,7 @@ func (security *Security) MsgCheck(in *MsgCheckRequest) (res MsgCheckResponse, e
req.Version = 2
uri := fmt.Sprintf(msgCheckURL, accessToken)
response, err := util.PostJSON(uri, req)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return
}

View File

@@ -12,22 +12,30 @@ const (
// 发送订阅消息
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
subscribeSendURL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send"
// 获取当前帐号下的个人模板列表
// 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"
// 添加订阅模板
// 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
uniformMessageSend = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send"
// getCategoryURL 获取类目
getCategoryURL = "https://api.weixin.qq.com/wxaapi/newtmpl/getcategory?access_token=%s"
// getPubTemplateKeyWordsByIDURL 获取关键词列表
getPubTemplateKeyWordsByIDURL = "https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatekeywords?access_token=%s&tid=%s"
// getPubTemplateTitleListURL 获取所属类目下的公共模板
getPubTemplateTitleListURL = "https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatetitles?access_token=%s&ids=%s&start=%d&limit=%d"
// setUserNotifyURL 激活与更新服务卡片
setUserNotifyURL = "https://api.weixin.qq.com/wxa/set_user_notify?access_token=%s"
// setUserNotifyExtURL 更新服务卡片扩展信息
setUserNotifyExtURL = "https://api.weixin.qq.com/wxa/set_user_notifyext?access_token=%s"
// getUserNotifyURL 查询服务卡片状态
getUserNotifyURL = "https://api.weixin.qq.com/wxa/get_user_notify?access_token=%s"
)
// Subscribe 订阅消息
@@ -58,11 +66,18 @@ type DataItem struct {
// TemplateItem template item
type TemplateItem struct {
PriTmplID string `json:"priTmplId"`
Title string `json:"title"`
Content string `json:"content"`
Example string `json:"example"`
Type int64 `json:"type"`
PriTmplID string `json:"priTmplId"`
Title string `json:"title"`
Content string `json:"content"`
Example string `json:"example"`
Type int64 `json:"type"`
KeywordEnumValueList []KeywordEnumValue `json:"keywordEnumValueList"`
}
// KeywordEnumValue 枚举参数值范围
type KeywordEnumValue struct {
EnumValueList []string `json:"enumValueList"`
KeywordCode string `json:"keywordCode"`
}
// TemplateList template list
@@ -224,3 +239,200 @@ func (s *Subscribe) Delete(templateID string) (err error) {
}
return util.DecodeWithCommonError(response, "DeleteSubscribe")
}
// GetCategoryResponse 获取类目响应
type GetCategoryResponse struct {
util.CommonError
Data []Category `json:"data"`
}
// Category 类目
type Category struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
// GetCategory 获取类目
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/getCategory.html
func (s *Subscribe) GetCategory() ([]Category, error) {
var (
accessToken string
err error
)
if accessToken, err = s.GetAccessToken(); err != nil {
return nil, err
}
var response []byte
if response, err = util.HTTPGet(fmt.Sprintf(getCategoryURL, accessToken)); err != nil {
return nil, err
}
result := &GetCategoryResponse{}
err = util.DecodeWithError(response, result, "GetCategory")
return result.Data, err
}
// GetPubTemplateKeywordsByIDResponse 获取关键词列表响应
type GetPubTemplateKeywordsByIDResponse struct {
util.CommonError
Count int64 `json:"count"`
Data []PubTemplateKeywords `json:"data"`
}
// PubTemplateKeywords 关键词
type PubTemplateKeywords struct {
KID int64 `json:"kid"`
Name string `json:"name"`
Example string `json:"example"`
Rule string `json:"rule"`
}
// GetPubTemplateKeywordsByID 获取关键词列表
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/getPubTemplateKeyWordsById.html
func (s *Subscribe) GetPubTemplateKeywordsByID(tid string) (*GetPubTemplateKeywordsByIDResponse, error) {
var (
accessToken string
err error
)
if accessToken, err = s.GetAccessToken(); err != nil {
return nil, err
}
var response []byte
if response, err = util.HTTPGet(fmt.Sprintf(getPubTemplateKeyWordsByIDURL, accessToken, tid)); err != nil {
return nil, err
}
result := &GetPubTemplateKeywordsByIDResponse{}
err = util.DecodeWithError(response, result, "GetPubTemplateKeywordsByID")
return result, err
}
// GetPubTemplateTitleListRequest 获取所属类目下的公共模板请求
type GetPubTemplateTitleListRequest struct {
Start int64
Limit int64
IDs string
}
// GetPubTemplateTitleListResponse 获取所属类目下的公共模板响应
type GetPubTemplateTitleListResponse struct {
util.CommonError
Count int64 `json:"count"`
Data []PubTemplateTitle `json:"data"`
}
// PubTemplateTitle 模板标题
type PubTemplateTitle struct {
Type int64 `json:"type"`
TID string `json:"tid"`
Title string `json:"title"`
CategoryID string `json:"categoryId"`
}
// GetPubTemplateTitleList 获取所属类目下的公共模板
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/getPubTemplateTitleList.html
func (s *Subscribe) GetPubTemplateTitleList(req *GetPubTemplateTitleListRequest) (*GetPubTemplateTitleListResponse, error) {
var (
accessToken string
err error
)
if accessToken, err = s.GetAccessToken(); err != nil {
return nil, err
}
var response []byte
if response, err = util.HTTPGet(fmt.Sprintf(getPubTemplateTitleListURL, accessToken, req.IDs, req.Start, req.Limit)); err != nil {
return nil, err
}
result := &GetPubTemplateTitleListResponse{}
err = util.DecodeWithError(response, result, "GetPubTemplateTitleList")
return result, err
}
// SetUserNotifyRequest 激活与更新服务卡片请求
type SetUserNotifyRequest struct {
OpenID string `json:"openid"`
NotifyType int64 `json:"notify_type"`
NotifyCode string `json:"notify_code"`
ContentJSON string `json:"content_json"`
CheckJSON string `json:"check_json,omitempty"`
}
// SetUserNotify 激活与更新服务卡片
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/setUserNotify.html
func (s *Subscribe) SetUserNotify(req *SetUserNotifyRequest) error {
var (
accessToken string
err error
)
if accessToken, err = s.GetAccessToken(); err != nil {
return err
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(setUserNotifyURL, accessToken), req); err != nil {
return err
}
return util.DecodeWithCommonError(response, "SetUserNotify")
}
// SetUserNotifyExtRequest 更新服务卡片扩展信息请求
type SetUserNotifyExtRequest struct {
OpenID string `json:"openid"`
NotifyType int64 `json:"notify_type"`
NotifyCode string `json:"notify_code"`
ExtJSON string `json:"ext_json"`
}
// SetUserNotifyExt 更新服务卡片扩展信息
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/setUserNotifyExt.html
func (s *Subscribe) SetUserNotifyExt(req *SetUserNotifyExtRequest) error {
var (
accessToken string
err error
)
if accessToken, err = s.GetAccessToken(); err != nil {
return err
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(setUserNotifyExtURL, accessToken), req); err != nil {
return err
}
return util.DecodeWithCommonError(response, "SetUserNotifyExt")
}
// GetUserNotifyRequest 查询服务卡片状态请求
type GetUserNotifyRequest struct {
OpenID string `json:"openid"`
NotifyType int64 `json:"notify_type"`
NotifyCode string `json:"notify_code"`
}
// GetUserNotifyResponse 查询服务卡片状态响应
type GetUserNotifyResponse struct {
util.CommonError
NotifyInfo NotifyInfo `json:"notify_info"`
}
// NotifyInfo 卡片状态
type NotifyInfo struct {
NotifyType int64 `json:"notify_type"`
ContentJSON string `json:"content_json"`
CodeState int64 `json:"code_state"`
CodeExpireTime int64 `json:"code_expire_time"`
}
// GetUserNotify 查询服务卡片状态
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/getUserNotify.html
func (s *Subscribe) GetUserNotify(req *GetUserNotifyRequest) (*GetUserNotifyResponse, error) {
var (
accessToken string
err error
)
if accessToken, err = s.GetAccessToken(); err != nil {
return nil, err
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(getUserNotifyURL, accessToken), req); err != nil {
return nil, err
}
result := &GetUserNotifyResponse{}
err = util.DecodeWithError(response, result, "GetUserNotify")
return result, err
}

View File

@@ -6,7 +6,13 @@ import (
"github.com/silenceper/wechat/v2/util"
)
const queryURL = "https://api.weixin.qq.com/wxa/query_urllink"
const queryURL = "https://api.weixin.qq.com/wxa/query_urllink?access_token=%s"
// ULQueryRequest 查询加密URLLink请求
type ULQueryRequest struct {
URLLink string `json:"url_link"`
QueryType int `json:"query_type"`
}
// ULQueryResult 返回的结果
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.query.html 返回值
@@ -28,25 +34,35 @@ type ULQueryResult struct {
ResourceAppid string `json:"resource_appid"`
} `json:"cloud_base"`
} `json:"url_link_info"`
VisitOpenid string `json:"visit_openid"`
VisitOpenid string `json:"visit_openid"`
QuotaInfo QuotaInfo `json:"quota_info"`
}
// QuotaInfo quota 配置
type QuotaInfo struct {
RemainVisitQuota int64 `json:"remain_visit_quota"`
}
// Query 查询小程序 url_link 配置。
func (u *URLLink) Query(urlLink string) (*ULQueryResult, error) {
accessToken, err := u.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", queryURL, accessToken)
response, err := util.PostJSON(uri, map[string]string{"url_link": urlLink})
if err != nil {
return nil, err
}
var resp ULQueryResult
err = util.DecodeWithError(response, &resp, "URLLink.Query")
if err != nil {
return nil, err
}
return &resp, nil
return u.QueryWithType(&ULQueryRequest{URLLink: urlLink})
}
// QueryWithType 查询加密URLLink
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-link/queryUrlLink.html
func (u *URLLink) QueryWithType(req *ULQueryRequest) (*ULQueryResult, error) {
var (
accessToken string
err error
)
if accessToken, err = u.GetAccessToken(); err != nil {
return nil, err
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(queryURL, accessToken), req); err != nil {
return nil, err
}
result := &ULQueryResult{}
err = util.DecodeWithError(response, result, "URLLink.Query")
return result, err
}

View File

@@ -14,7 +14,8 @@ const (
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
type QueryScheme struct {
// 小程序 scheme 码
Scheme string `json:"scheme"`
Scheme string `json:"scheme"`
QueryType int `json:"query_type"`
}
// SchemeInfo scheme 配置
@@ -33,34 +34,47 @@ type SchemeInfo struct {
EnvVersion EnvVersion `json:"env_version"`
}
// resQueryScheme 返回结构体
// QuotaInfo quota 配置
type QuotaInfo struct {
RemainVisitQuota int64 `json:"remain_visit_quota"`
}
// ResQueryScheme 返回结构体
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
type resQueryScheme struct {
type ResQueryScheme struct {
// 通用错误
util.CommonError
// scheme 配置
SchemeInfo SchemeInfo `json:"scheme_info"`
// 访问该链接的openid没有用户访问过则为空字符串
VisitOpenid string `json:"visit_openid"`
VisitOpenid string `json:"visit_openid"`
QuotaInfo QuotaInfo `json:"quota_info"`
}
// QueryScheme 查询小程序 scheme 码
func (u *URLScheme) QueryScheme(querySchemeParams QueryScheme) (schemeInfo SchemeInfo, visitOpenid string, err error) {
var accessToken string
accessToken, err = u.GetAccessToken()
res, err := u.QuerySchemeWithRes(querySchemeParams)
if err != nil {
return
}
urlStr := fmt.Sprintf(querySchemeURL, accessToken)
var response []byte
response, err = util.PostJSON(urlStr, querySchemeParams)
if err != nil {
return
}
// 使用通用方法返回错误
var res resQueryScheme
err = util.DecodeWithError(response, &res, "QueryScheme")
return res.SchemeInfo, res.VisitOpenid, err
}
// QuerySchemeWithRes 查询scheme码
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-scheme/queryScheme.html
func (u *URLScheme) QuerySchemeWithRes(req QueryScheme) (*ResQueryScheme, error) {
var (
accessToken string
err error
)
if accessToken, err = u.GetAccessToken(); err != nil {
return nil, err
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(querySchemeURL, accessToken), req); err != nil {
return nil, err
}
result := &ResQueryScheme{}
err = util.DecodeWithError(response, result, "QueryScheme")
return result, err
}

View File

@@ -17,7 +17,12 @@ func NewURLScheme(ctx *context.Context) *URLScheme {
return &URLScheme{Context: ctx}
}
const generateURL = "https://api.weixin.qq.com/wxa/generatescheme"
const (
// generateURL 获取加密scheme码
generateURL = "https://api.weixin.qq.com/wxa/generatescheme"
// generateNFCURL 获取 NFC 的小程序 scheme
generateNFCURL = "https://api.weixin.qq.com/wxa/generatenfcscheme?access_token=%s"
)
// TExpireType 失效类型 (指定时间戳/指定间隔)
type TExpireType int
@@ -50,11 +55,13 @@ type JumpWxa struct {
// USParams 请求参数
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html#请求参数
type USParams struct {
JumpWxa *JumpWxa `json:"jump_wxa"`
ExpireType TExpireType `json:"expire_type"`
ExpireTime int64 `json:"expire_time"`
ExpireInterval int `json:"expire_interval"`
JumpWxa *JumpWxa `json:"jump_wxa,omitempty"`
ExpireType TExpireType `json:"expire_type,omitempty"`
ExpireTime int64 `json:"expire_time,omitempty"`
ExpireInterval int `json:"expire_interval,omitempty"`
IsExpire bool `json:"is_expire,omitempty"`
ModelID string `json:"model_id,omitempty"`
Sn string `json:"sn,omitempty"`
}
// USResult 返回的结果
@@ -81,3 +88,22 @@ func (u *URLScheme) Generate(params *USParams) (string, error) {
err = util.DecodeWithError(response, &resp, "URLScheme.Generate")
return resp.OpenLink, err
}
// GenerateNFC 获取 NFC 的小程序 scheme
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-scheme/generateNFCScheme.html
func (u *URLScheme) GenerateNFC(params *USParams) (string, error) {
var (
accessToken string
err error
)
if accessToken, err = u.GetAccessToken(); err != nil {
return "", err
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(generateNFCURL, accessToken), params); err != nil {
return "", err
}
result := &USResult{}
err = util.DecodeWithError(response, result, "URLScheme.GenerateNFC")
return result.OpenLink, 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

@@ -68,3 +68,18 @@ func DecodeWithError(response []byte, obj interface{}, apiName string) error {
}
return nil
}
// HandleFileResponse 通用处理微信等接口返回:有时 JSON 错误,有时文件内容
func HandleFileResponse(response []byte, apiName string) ([]byte, error) {
var commErr CommonError
if err := json.Unmarshal(response, &commErr); err == nil {
// 能解析成 JSON判断是否为错误
if commErr.ErrCode != 0 {
commErr.apiName = apiName
return nil, &commErr
}
// 能解析成 JSON 且没错误码,极少情况(比如微信返回的业务数据是 JSON 但无 errcode 字段),可根据需要调整
}
// 不能解析成 JSON或没错误码直接返回原始内容
return response, nil
}

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)
@@ -290,7 +291,20 @@ func httpWithTLS(rootCa, key string) (*http.Client, error) {
config := &tls.Config{
Certificates: []tls.Certificate{cert},
}
trans := (DefaultHTTPClient.Transport.(*http.Transport)).Clone()
// 安全地获取 *http.Transport
var trans *http.Transport
// 尝试从 DefaultHTTPClient 获取 Transport如果失败则使用默认值
if DefaultHTTPClient.Transport != nil {
if t, ok := DefaultHTTPClient.Transport.(*http.Transport); ok {
trans = t.Clone()
}
}
// 如果无法获取有效的 Transport使用默认值
if trans == nil {
trans = http.DefaultTransport.(*http.Transport).Clone()
}
trans.TLSClientConfig = config
trans.DisableCompression = true
client = &http.Client{Transport: trans}

81
util/http_test.go Normal file
View File

@@ -0,0 +1,81 @@
package util
import (
"net/http"
"testing"
)
// TestHttpWithTLS_NilTransport tests the scenario where DefaultHTTPClient.Transport is nil
func TestHttpWithTLS_NilTransport(t *testing.T) {
// Save original transport
originalTransport := DefaultHTTPClient.Transport
defer func() {
DefaultHTTPClient.Transport = originalTransport
}()
// Set Transport to nil to simulate the bug scenario
DefaultHTTPClient.Transport = nil
// This should not panic after the fix
// Note: This will fail due to invalid cert path, but shouldn't panic on type assertion
_, err := httpWithTLS("./testdata/invalid_cert.p12", "password")
// We expect an error (cert file not found), but NOT a panic
if err == nil {
t.Error("Expected error due to invalid cert path, but got nil")
}
}
// TestHttpWithTLS_CustomTransport tests the scenario where DefaultHTTPClient has a custom Transport
func TestHttpWithTLS_CustomTransport(t *testing.T) {
// Save original transport
originalTransport := DefaultHTTPClient.Transport
defer func() {
DefaultHTTPClient.Transport = originalTransport
}()
// Set a custom http.Transport
customTransport := &http.Transport{
MaxIdleConns: 100,
}
DefaultHTTPClient.Transport = customTransport
// This should not panic
_, err := httpWithTLS("./testdata/invalid_cert.p12", "password")
// We expect an error (cert file not found), but NOT a panic
if err == nil {
t.Error("Expected error due to invalid cert path, but got nil")
}
}
// CustomRoundTripper is a custom implementation of http.RoundTripper
type CustomRoundTripper struct{}
func (c *CustomRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return http.DefaultTransport.RoundTrip(req)
}
// TestHttpWithTLS_CustomRoundTripper tests the edge case where DefaultHTTPClient has a custom RoundTripper
// that is NOT *http.Transport
func TestHttpWithTLS_CustomRoundTripper(t *testing.T) {
// Save original transport
originalTransport := DefaultHTTPClient.Transport
defer func() {
DefaultHTTPClient.Transport = originalTransport
}()
// Set a custom RoundTripper that is NOT *http.Transport
customRoundTripper := &CustomRoundTripper{}
DefaultHTTPClient.Transport = customRoundTripper
// Create a recovery handler to catch potential panic
defer func() {
if r := recover(); r != nil {
t.Errorf("httpWithTLS panicked with custom RoundTripper: %v", r)
}
}()
// This might panic if the code doesn't handle non-*http.Transport RoundTripper properly
_, _ = httpWithTLS("./testdata/invalid_cert.p12", "password")
}

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
}

View File

@@ -176,6 +176,7 @@ type BatchGetExternalUserDetailsRequest struct {
type ExternalUserDetailListResponse struct {
util.CommonError
ExternalContactList []ExternalUserForBatch `json:"external_contact_list"`
NextCursor string `json:"next_cursor"`
}
// ExternalUserForBatch 批量获取外部联系人客户列表
@@ -214,23 +215,23 @@ type FollowInfo struct {
// BatchGetExternalUserDetails 批量获取外部联系人详情
// @see https://developer.work.weixin.qq.com/document/path/92994
func (r *Client) BatchGetExternalUserDetails(request BatchGetExternalUserDetailsRequest) ([]ExternalUserForBatch, error) {
func (r *Client) BatchGetExternalUserDetails(request BatchGetExternalUserDetailsRequest) ([]ExternalUserForBatch, string, error) {
accessToken, err := r.GetAccessToken()
if err != nil {
return nil, err
return nil, "", err
}
var response []byte
jsonData, err := json.Marshal(request)
if err != nil {
return nil, err
return nil, "", err
}
response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", fetchBatchExternalContactUserDetailURL, accessToken), string(jsonData))
if err != nil {
return nil, err
return nil, "", err
}
var result ExternalUserDetailListResponse
err = util.DecodeWithError(response, &result, "BatchGetExternalUserDetails")
return result.ExternalContactList, err
return result.ExternalContactList, result.NextCursor, err
}
// UpdateUserRemarkRequest 修改客户备注信息请求体

View File

@@ -8,10 +8,10 @@ import (
// SignatureOptions 微信服务器验证参数
type SignatureOptions struct {
Signature string `form:"msg_signature"`
TimeStamp string `form:"timestamp"`
Nonce string `form:"nonce"`
EchoStr string `form:"echostr"`
Signature string `form:"msg_signature" json:"msg_signature"`
TimeStamp string `form:"timestamp" json:"timestamp"`
Nonce string `form:"nonce" json:"nonce"`
EchoStr string `form:"echostr" json:"echostr"`
}
// VerifyURL 验证请求参数是否合法并返回解密后的消息内容

View File

@@ -191,12 +191,6 @@ func (r *Client) GetTempFile(mediaID string) ([]byte, error) {
return nil, err
}
// 检查响应是否为错误信息
err = util.DecodeWithCommonError(response, "GetTempFile")
if err != nil {
return nil, err
}
// 如果不是错误响应,则返回原始数据
return response, nil
// 检查响应是否为错误信息,如果不是错误响应,则返回原始数据
return util.HandleFileResponse(response, "GetTempFile")
}