Compare commits

...
49 Commits
Author SHA1 Message Date
GargantuaX 2bc22dc4ae feat: add aispeech dialog APIs (#887)
* feat: add aispeech dialog APIs

* test: fix aispeech lint failures

* test: satisfy aispeech interfacer lint
2026-07-20 14:10:56 +08:00
劣头andSuperX 0e34286d78 fix(external_user): 修复外部联系人结构体字段类型并格式化对齐 (#890)
将ExternalProfile字段从字符串类型修正为指针结构体类型,适配企业微信API返回的JSON对象格式;同时对齐所有结构体字段的JSON标签,提升代码可读性

Co-authored-by: SuperX <xiang.song@todreamer.com>
2026-06-23 14:44:44 +08:00
muzimu fb3009e337 fix: 修复memory cache并发问题 (#889) 2026-06-15 18:11:33 +08:00
wwek d34af7aeeb feat(virtualpayment): ✨ 添加 postJSONBytesContext 函数并更新相关调用 解决body签名不一致问题 (#886)
* fix typo

* feat(virtualpayment): ✨ 添加广告金相关接口及错误码

- 新增批量任务、退款、广告金充值等相关错误码
- 添加下载订单、查询商家账户余额、查询广告金发放记录等接口
- 实现投诉处理相关接口,包括获取投诉列表、投诉详情、协商历史等
- 完善上传媒体文件及获取签名头部的功能

* feat(virtualpayment): ✨ 优化请求地址组合逻辑,合并多个 case 语句

* 补充虚拟支付道具发货拼团字段

* fix(virtualpayment): 🐛 修正退款错误码注释拼写和商户订单号字段名称

* feat(virtualpayment): ✨ 添加 iOS Apple 支付退款问询事件及相关数据结构

* feat(virtualpayment): ✨ 优化 iOS Apple 支付退款问询事件的注释格式

* feat(virtualpayment): ✨ 添加 postJSONBytesContext 函数并更新相关调用

* fix(miniprogram): ShowSplashAd 传 false 时未序列化发送到微信 API

将 ShowSplashAd 从 bool 改为 *bool,解决 omitempty 导致 false 值
被当作零值省略的问题。微信 API 默认值为 true(展示广告),传 false
无法关闭封面广告。

Closes #882
2026-06-03 23:31:15 +08:00
wwek bfbfc844ce feat(virtualpayment): 虚拟支付对齐微信最新api接口文档 (#885)
* fix typo

* feat(virtualpayment): ✨ 添加广告金相关接口及错误码

- 新增批量任务、退款、广告金充值等相关错误码
- 添加下载订单、查询商家账户余额、查询广告金发放记录等接口
- 实现投诉处理相关接口,包括获取投诉列表、投诉详情、协商历史等
- 完善上传媒体文件及获取签名头部的功能

* feat(virtualpayment): ✨ 优化请求地址组合逻辑,合并多个 case 语句

* 补充虚拟支付道具发货拼团字段

* fix(virtualpayment): 🐛 修正退款错误码注释拼写和商户订单号字段名称

* feat(virtualpayment): ✨ 添加 iOS Apple 支付退款问询事件及相关数据结构

* feat(virtualpayment): ✨ 优化 iOS Apple 支付退款问询事件的注释格式
2026-05-22 01:00:39 +08:00
vvfanandniuweifan 269d93ea6f fix: avoid using empty jsapi ticket from cache (#883)
Co-authored-by: niuweifan <niuweifan@66nao.com>
2026-05-18 22:43:20 +08:00
markwang e4fc0bd615 feat: 企业微信-客户联系-结束临时会话 (#878) 2026-02-11 14:22:54 +08:00
markwang 20a8183e88 feat: 企业微信-登录二次验证 (#877) 2026-01-27 09:57:57 +08:00
曹晶andlumiaqian f0f35e2f77 feat(miniprogram): add intracity delivery service APIs (#875)
* feat(media): add getTempFile api

add getTempFile api

* feat(miniprogram): add intracity delivery service APIs

Add WeChat intracity (same-city) delivery service APIs including:
- Store management: Apply, Create, Query, Update store
- Payment: StoreCharge, StoreRefund, QueryFlow, BalanceQuery
- Order management: PreAddOrder, AddOrder, QueryOrder, CancelOrder

Ref: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/intracity_service.html

---------

Co-authored-by: lumiaqian <cjj1@truesightai.com>
2026-01-22 19:18:52 +08:00
markwang eebfb93386 feat: 企业微信-获取成员UserID (#876) 2026-01-22 19:18:35 +08:00
markwang 9b028e5368 feat: 企业微信-通讯录管理-成员管理新增接口 (#874)
* feat: 企业微信-通讯录管理-成员管理新增接口

* feat: 企业微信-通讯录管理-成员管理新增接口
2026-01-20 16:15:09 +08:00
markwang 2bfc250f21 feat: 企业微信-通讯录管理-成员管理新增接口 (#873) 2026-01-16 23:31:39 +08:00
markwang f0866babb5 feat: 企业微信-打卡规则支持配置大小周规则 (#872) 2026-01-14 12:54:39 +08:00
Gophlet 7d93d1b9c8 feat(redis): 优化配置语义并增强超时与连接池能力,同时保持向后兼容 (#869)
* fix: correct non-standard 'yml' tag to 'yaml' in RedisOpts

* fix: apply MaxActive config to Redis PoolSize

* fix: clarify config semantics, enhance timeout & pool options, and maintain backward compatibility

* test: update unit test for redis

* refactor: apply suggestions from code review

* test: add comprehensive coverage for redis

* refactor: resolve funlen linter errors in redis unit tests

* refactor: remove empty else-if branch in NewRedis function
2026-01-08 09:49:41 +08:00
markwang 78c00a9124 fix: 企业微信-营销获客更新 (#871) 2025-12-30 18:00:01 +08:00
yin jiashuaiand阴佳帅 54f08cbd73 update:小程序ocr识别 refs#864 (#865)
* update:小程序ocr识别

* fix:删除ResPlateNumber

---------

Co-authored-by: 阴佳帅 <14988374+yin-jiashuai@user.noreply.gitee.com>
2025-11-16 21:46:49 +08:00
markwang c34ff2031b fix: 企业微信-通讯录管理-更新成员企业邮箱别名参数类型修正 (#863) 2025-11-13 14:13:14 +08:00
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
zhangjianiandtax 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
zhangjianiandtax 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
曹晶andcaojing 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
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
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
65 changed files with 6372 additions and 222 deletions

No files matched your search

+2 -2
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
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 }}
+12 -5
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)
+100 -2
View File
@@ -1,5 +1,103 @@
# 智能对话
[官方文档](https://developers.weixin.qq.com/doc/aispeech/platform/INTERFACEDOCUMENT.html)
[官方文档](https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/token.html)
## 快速入门
## 快速入门
```go
import (
"github.com/silenceper/wechat/v2"
"github.com/silenceper/wechat/v2/aispeech/config"
"github.com/silenceper/wechat/v2/aispeech/dialog"
"github.com/silenceper/wechat/v2/cache"
)
wc := wechat.NewWechat()
memory := cache.NewMemory()
ai := wc.GetAISpeech(&config.Config{
AppID: "xxx",
Token: "xxx",
AESKey: "xxx",
Account: "admin",
Cache: memory,
})
dialogClient := ai.GetDialog()
accessToken, err := dialogClient.GetAccessToken()
if err != nil {
return err
}
res, err := dialogClient.Query(&dialog.QueryRequest{
Query: "你好",
Env: "online",
UserID: "user-1",
})
```
## 简单问答导入与发布
```go
task, err := dialogClient.ImportJSON(&dialog.ImportJSONRequest{
Mode: 0,
Data: []dialog.BotIntent{{
Skill: "售前咨询",
Intent: "查询营业时间",
Disable: false,
Questions: []string{"你们几点开门", "营业时间是什么时候"},
Answers: []string{"我们的营业时间是周一至周五 9:00-18:00"},
}},
})
if err != nil {
return err
}
asyncResult, err := dialogClient.FetchAsync(&dialog.FetchAsyncRequest{
TaskID: task.TaskID,
})
if err != nil {
return err
}
publish, err := dialogClient.Publish()
if err != nil {
return err
}
progress, err := dialogClient.GetEffectiveProgress(&dialog.EffectiveProgressRequest{
Env: "online",
})
```
当前封装微信智能对话开放接口中“接入智能对话”的 6 个接口:
- `POST /v2/token`
- `POST /v2/bot/import/json`
- `POST /v2/async/fetch`
- `POST /v2/bot/publish`
- `POST /v2/bot/effective_progress`
- `POST /v2/bot/query`
## 真实接口验证
默认单元测试不会访问真实微信接口。如需验证 token 和 query 只读链路,可设置以下环境变量后运行:
```bash
AISPEECH_INTEGRATION=1
AISPEECH_APPID=xxx
AISPEECH_TOKEN=xxx
AISPEECH_AES_KEY=xxx
go test ./aispeech/dialog -run TestIntegrationAccessTokenAndQuery
```
如需验证导入、异步任务查询、发布、发布进度查询和命中回答的完整链路,可显式开启变更型测试。该测试会导入一个 `CodexSmokeTest` 临时问答并发布到线上机器人:
```bash
AISPEECH_INTEGRATION_MUTATION=1
AISPEECH_APPID=xxx
AISPEECH_TOKEN=xxx
AISPEECH_AES_KEY=xxx
go test ./aispeech/dialog -run TestIntegrationFullDialogFlow -v
```
+60
View File
@@ -0,0 +1,60 @@
package aispeech
import (
stdcontext "context"
"github.com/silenceper/wechat/v2/aispeech/config"
"github.com/silenceper/wechat/v2/aispeech/context"
"github.com/silenceper/wechat/v2/aispeech/dialog"
"github.com/silenceper/wechat/v2/credential"
)
// AISpeech 微信智能对话相关 API.
type AISpeech struct {
ctx *context.Context
dialog *dialog.Dialog
}
// NewAISpeech 实例化智能对话 API.
func NewAISpeech(cfg *config.Config) *AISpeech {
ctx := &context.Context{
Config: cfg,
AccessTokenContextHandle: dialog.NewAccessToken(cfg),
}
return &AISpeech{ctx: ctx}
}
// GetContext get Context.
func (a *AISpeech) GetContext() *context.Context {
return a.ctx
}
// SetAccessTokenHandle 自定义 access_token 获取方式.
func (a *AISpeech) SetAccessTokenHandle(accessTokenHandle credential.AccessTokenHandle) {
a.ctx.AccessTokenContextHandle = credential.AccessTokenCompatibleHandle{
AccessTokenHandle: accessTokenHandle,
}
}
// SetAccessTokenContextHandle 自定义 access_token 获取方式.
func (a *AISpeech) SetAccessTokenContextHandle(accessTokenContextHandle credential.AccessTokenContextHandle) {
a.ctx.AccessTokenContextHandle = accessTokenContextHandle
}
// GetAccessToken 获取 access token.
func (a *AISpeech) GetAccessToken() (string, error) {
return a.ctx.GetAccessToken()
}
// GetAccessTokenContext 获取 access token.
func (a *AISpeech) GetAccessTokenContext(ctx stdcontext.Context) (string, error) {
return a.ctx.GetAccessTokenContext(ctx)
}
// GetDialog 获取对话平台 API.
func (a *AISpeech) GetDialog() *dialog.Dialog {
if a.dialog == nil {
a.dialog = dialog.NewDialog(a.ctx)
}
return a.dialog
}
+61
View File
@@ -0,0 +1,61 @@
package aispeech
import (
stdcontext "context"
"testing"
"github.com/silenceper/wechat/v2/aispeech/config"
"github.com/silenceper/wechat/v2/cache"
)
type staticAccessToken struct {
token string
}
func (s staticAccessToken) GetAccessToken() (string, error) {
return s.token, nil
}
type staticAccessTokenContext struct {
token string
}
type contextTokenKey struct{}
func (s staticAccessTokenContext) GetAccessToken() (string, error) {
return s.GetAccessTokenContext(stdcontext.Background())
}
func (s staticAccessTokenContext) GetAccessTokenContext(ctx stdcontext.Context) (string, error) {
if v := ctx.Value(contextTokenKey{}); v != nil {
return v.(string), nil
}
return s.token, nil
}
func TestSetAccessTokenHandle(t *testing.T) {
ai := NewAISpeech(&config.Config{Cache: cache.NewMemory()})
ai.SetAccessTokenHandle(staticAccessToken{token: "custom-token"})
token, err := ai.GetAccessToken()
if err != nil {
t.Fatalf("GetAccessToken error: %v", err)
}
if token != "custom-token" {
t.Fatalf("bad token: %s", token)
}
}
func TestSetAccessTokenContextHandle(t *testing.T) {
ai := NewAISpeech(&config.Config{Cache: cache.NewMemory()})
ai.SetAccessTokenContextHandle(staticAccessTokenContext{token: "custom-token"})
ctx := stdcontext.WithValue(stdcontext.Background(), contextTokenKey{}, "context-token")
token, err := ai.GetAccessTokenContext(ctx)
if err != nil {
t.Fatalf("GetAccessTokenContext error: %v", err)
}
if token != "context-token" {
t.Fatalf("bad token: %s", token)
}
}
+23
View File
@@ -0,0 +1,23 @@
package config
import "github.com/silenceper/wechat/v2/cache"
const defaultBaseURL = "https://openaiapi.weixin.qq.com"
// Config for 微信智能对话.
type Config struct {
AppID string `json:"app_id"`
Token string `json:"token"`
AESKey string `json:"aes_key"`
Account string `json:"account"`
BaseURL string `json:"base_url"`
Cache cache.Cache
}
// GetBaseURL returns the API base URL.
func (cfg *Config) GetBaseURL() string {
if cfg.BaseURL == "" {
return defaultBaseURL
}
return cfg.BaseURL
}
+15
View File
@@ -0,0 +1,15 @@
package config
import "testing"
func TestGetBaseURL(t *testing.T) {
cfg := &Config{}
if cfg.GetBaseURL() != defaultBaseURL {
t.Fatalf("bad default base url: %s", cfg.GetBaseURL())
}
cfg.BaseURL = "http://example.com"
if cfg.GetBaseURL() != "http://example.com" {
t.Fatalf("bad custom base url: %s", cfg.GetBaseURL())
}
}
+12
View File
@@ -0,0 +1,12 @@
package context
import (
"github.com/silenceper/wechat/v2/aispeech/config"
"github.com/silenceper/wechat/v2/credential"
)
// Context struct
type Context struct {
*config.Config
credential.AccessTokenContextHandle
}
+116
View File
@@ -0,0 +1,116 @@
package dialog
import (
stdcontext "context"
"encoding/json"
"github.com/silenceper/wechat/v2/aispeech/encryptor"
)
// GetAccessToken 获取 access token.
func (d *Dialog) GetAccessToken() (string, error) {
return d.Context.GetAccessToken()
}
// GetAccessTokenContext 获取 access token.
func (d *Dialog) GetAccessTokenContext(ctx stdcontext.Context) (string, error) {
return d.Context.GetAccessTokenContext(ctx)
}
// ImportJSON 简单问答导入.
func (d *Dialog) ImportJSON(req *ImportJSONRequest) (*ImportJSONResponse, error) {
return d.ImportJSONContext(stdcontext.Background(), req)
}
// ImportJSONContext 简单问答导入.
func (d *Dialog) ImportJSONContext(ctx stdcontext.Context, req *ImportJSONRequest) (*ImportJSONResponse, error) {
var res ImportJSONResponse
requestID, err := d.postJSON(ctx, importJSONPath, req, &res, "AISpeechImportJSON")
res.RequestID = requestID
return &res, err
}
// Publish 发布机器人.
func (d *Dialog) Publish() (*PublishResponse, error) {
return d.PublishContext(stdcontext.Background())
}
// PublishContext 发布机器人.
func (d *Dialog) PublishContext(ctx stdcontext.Context) (*PublishResponse, error) {
var res PublishResponse
requestID, err := d.postEmpty(ctx, publishPath, &res, "AISpeechPublish")
res.RequestID = requestID
return &res, err
}
// GetEffectiveProgress 查询机器人发布进度.
func (d *Dialog) GetEffectiveProgress(req *EffectiveProgressRequest) (*EffectiveProgressResponse, error) {
return d.GetEffectiveProgressContext(stdcontext.Background(), req)
}
// GetEffectiveProgressContext 查询机器人发布进度.
func (d *Dialog) GetEffectiveProgressContext(ctx stdcontext.Context, req *EffectiveProgressRequest) (*EffectiveProgressResponse, error) {
var res EffectiveProgressResponse
requestID, err := d.postJSON(ctx, effectiveProgressPath, req, &res, "AISpeechGetEffectiveProgress")
res.RequestID = requestID
return &res, err
}
// FetchAsync 查询异步任务.
func (d *Dialog) FetchAsync(req *FetchAsyncRequest) (*FetchAsyncResponse, error) {
return d.FetchAsyncContext(stdcontext.Background(), req)
}
// FetchAsyncContext 查询异步任务.
func (d *Dialog) FetchAsyncContext(ctx stdcontext.Context, req *FetchAsyncRequest) (*FetchAsyncResponse, error) {
var res FetchAsyncResponse
requestID, err := d.postJSON(ctx, fetchAsyncPath, req, &res, "AISpeechFetchAsync")
res.RequestID = requestID
return &res, err
}
// Query 调用智能对话.
func (d *Dialog) Query(req *QueryRequest) (*QueryResponse, error) {
return d.QueryContext(stdcontext.Background(), req)
}
// QueryContext 调用智能对话.
func (d *Dialog) QueryContext(ctx stdcontext.Context, req *QueryRequest) (*QueryResponse, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
encryptedBody, err := encryptor.Encrypt(d.AESKey, body)
if err != nil {
return nil, err
}
accessToken, err := d.GetAccessTokenContext(ctx)
if err != nil {
return nil, err
}
if accessToken == "" {
return nil, errEmptyAccessToken
}
response, err := post(ctx, d.Config, queryPath, []byte(encryptedBody), "text/plain", accessToken, "")
if err != nil {
return nil, err
}
plainResponse := response
if !json.Valid(response) {
plainResponse, err = encryptor.Decrypt(d.AESKey, string(response))
if err != nil {
return nil, err
}
}
var res QueryResponse
requestID, err := decodeResponse(plainResponse, &res, "AISpeechQuery")
if err != nil {
return nil, err
}
res.RequestID = requestID
if json.Valid([]byte(res.Answer)) {
res.RawAnswer = json.RawMessage(res.Answer)
}
return &res, nil
}
+120
View File
@@ -0,0 +1,120 @@
package dialog
import (
stdcontext "context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/silenceper/wechat/v2/aispeech/config"
"github.com/silenceper/wechat/v2/aispeech/context"
"github.com/silenceper/wechat/v2/aispeech/encryptor"
"github.com/silenceper/wechat/v2/util"
)
const (
// tokenPath 获取 AccessToken.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/token.html
tokenPath = "/v2/token"
// importJSONPath 简单问答 JSON 导入.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/import.html
importJSONPath = "/v2/bot/import/json"
// fetchAsyncPath 异步任务查询.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/fetch.html
fetchAsyncPath = "/v2/async/fetch"
// publishPath 发布机器人.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/publish.html
publishPath = "/v2/bot/publish"
// effectiveProgressPath 查询机器人发布进度.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/progress.html
effectiveProgressPath = "/v2/bot/effective_progress"
// queryPath 调用智能对话.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/query.html
queryPath = "/v2/bot/query"
)
// Dialog 智能对话平台.
type Dialog struct {
*context.Context
}
// NewDialog init.
func NewDialog(ctx *context.Context) *Dialog {
return &Dialog{ctx}
}
func (d *Dialog) postJSON(ctx stdcontext.Context, path string, req interface{}, res interface{}, apiName string) (string, error) {
body, err := json.Marshal(req)
if err != nil {
return "", err
}
accessToken, err := d.GetAccessTokenContext(ctx)
if err != nil {
return "", err
}
if accessToken == "" {
return "", errEmptyAccessToken
}
response, err := post(ctx, d.Config, path, body, "application/json", accessToken, "")
if err != nil {
return "", err
}
return decodeResponse(response, res, apiName)
}
func (d *Dialog) postEmpty(ctx stdcontext.Context, path string, res interface{}, apiName string) (string, error) {
accessToken, err := d.GetAccessTokenContext(ctx)
if err != nil {
return "", err
}
if accessToken == "" {
return "", errEmptyAccessToken
}
response, err := post(ctx, d.Config, path, nil, "application/json", accessToken, "")
if err != nil {
return "", err
}
return decodeResponse(response, res, apiName)
}
func post(ctx stdcontext.Context, cfg *config.Config, path string, body []byte, contentType, accessToken, appID string) ([]byte, error) {
timestamp := time.Now().Unix()
nonce, err := randomString(16)
if err != nil {
return nil, err
}
requestID, err := randomString(32)
if err != nil {
return nil, err
}
header := map[string]string{
"request_id": requestID,
"timestamp": fmt.Sprintf("%d", timestamp),
"nonce": nonce,
"sign": encryptor.Sign(cfg.Token, timestamp, nonce, body),
"Content-Type": contentType,
}
if accessToken != "" {
header["X-OPENAI-TOKEN"] = accessToken
} else {
header["X-APPID"] = appID
}
return util.HTTPPostContext(ctx, buildURL(cfg.GetBaseURL(), path), body, header)
}
func buildURL(baseURL, path string) string {
return strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(path, "/")
}
func randomString(length int) (string, error) {
buf := make([]byte, (length+1)/2)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return hex.EncodeToString(buf)[:length], nil
}
+422
View File
@@ -0,0 +1,422 @@
package dialog
import (
stdcontext "context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"github.com/silenceper/wechat/v2/aispeech/config"
aispeechContext "github.com/silenceper/wechat/v2/aispeech/context"
"github.com/silenceper/wechat/v2/aispeech/encryptor"
"github.com/silenceper/wechat/v2/cache"
)
const (
testAccessToken = "access-token"
testAccount = "admin"
testAESKey = "q1Os1ZMe0nG28KUEx9lg3HjK7V5QyXvi212fzsgDqgz"
testAppID = "appid"
testImportRequest = "import-rid"
testImportTaskID = "task-import"
testPublishTaskID = "task-publish"
testQuery = "hello"
testQueryAnswer = "hello answer"
testQueryRequestID = "query-rid"
testToken = "token"
testTokenRequestID = "token-rid"
)
type emptyAccessTokenHandle struct{}
func (emptyAccessTokenHandle) GetAccessToken() (string, error) {
return "", nil
}
func (emptyAccessTokenHandle) GetAccessTokenContext(ctx stdcontext.Context) (string, error) {
return "", nil
}
func TestAccessTokenCache(t *testing.T) {
var tokenRequests int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenRequests++
body := readBody(t, r)
assertSign(t, r, testToken, body)
if r.Header.Get("X-APPID") != testAppID {
t.Fatalf("bad X-APPID: %s", r.Header.Get("X-APPID"))
}
_, _ = w.Write([]byte(accessTokenResponse("rid")))
}))
defer srv.Close()
ak := NewAccessToken(&config.Config{
AppID: testAppID,
Token: testToken,
Account: testAccount,
BaseURL: srv.URL,
Cache: cache.NewMemory(),
})
token, err := ak.GetAccessToken()
if err != nil {
t.Fatalf("GetAccessToken error: %v", err)
}
if token != testAccessToken {
t.Fatalf("bad token: %s", token)
}
token, err = ak.GetAccessToken()
if err != nil {
t.Fatalf("GetAccessToken second error: %v", err)
}
if token != testAccessToken {
t.Fatalf("bad token second: %s", token)
}
if tokenRequests != 1 {
t.Fatalf("token requests = %d, want 1", tokenRequests)
}
}
func TestAccessTokenCacheByAccount(t *testing.T) {
var tokenRequests int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenRequests++
body := readBody(t, r)
assertSign(t, r, testToken, body)
var req AccessTokenRequest
if err := json.Unmarshal(body, &req); err != nil {
t.Fatalf("bad token body: %v", err)
}
_, _ = w.Write([]byte(accountAccessTokenResponse(req.Account)))
}))
defer srv.Close()
memory := cache.NewMemory()
cfgA := testDialogConfig(srv.URL)
cfgA.Account = "admin-a"
cfgA.Cache = memory
cfgB := testDialogConfig(srv.URL)
cfgB.Account = "admin-b"
cfgB.Cache = memory
tokenA, err := NewAccessToken(cfgA).GetAccessToken()
if err != nil {
t.Fatalf("GetAccessToken A error: %v", err)
}
tokenB, err := NewAccessToken(cfgB).GetAccessToken()
if err != nil {
t.Fatalf("GetAccessToken B error: %v", err)
}
if tokenA != "admin-a-token" || tokenB != "admin-b-token" {
t.Fatalf("bad tokens: %s %s", tokenA, tokenB)
}
if tokenRequests != 2 {
t.Fatalf("token requests = %d, want 2", tokenRequests)
}
}
func TestAccessTokenEmptyToken(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"rid","data":{"access_token":""}}`))
}))
defer srv.Close()
ak := NewAccessToken(&config.Config{
AppID: testAppID,
Token: testToken,
BaseURL: srv.URL,
Cache: cache.NewMemory(),
})
if _, err := ak.GetAccessToken(); err == nil {
t.Fatal("GetAccessToken should return error")
}
}
func TestDialogAPIs(t *testing.T) {
srv := newDialogAPITestServer(t)
defer srv.Close()
d := newTestDialog(srv.URL)
assertDialogImportJSON(t, d)
assertDialogPublish(t, d)
assertDialogProgress(t, d)
assertDialogFetchAsync(t, d)
assertDialogQuery(t, d)
}
func TestDialogEmptyAccessToken(t *testing.T) {
d := NewDialog(&aispeechContext.Context{
Config: &config.Config{
AESKey: testAESKey,
},
AccessTokenContextHandle: emptyAccessTokenHandle{},
})
if _, err := d.ImportJSON(&ImportJSONRequest{}); err == nil {
t.Fatal("ImportJSON should return error")
}
if _, err := d.Publish(); err == nil {
t.Fatal("Publish should return error")
}
if _, err := d.Query(&QueryRequest{}); err == nil {
t.Fatal("Query should return error")
}
}
func TestDialogAPIError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case tokenPath:
_, _ = w.Write([]byte(accessTokenResponse(testTokenRequestID)))
case importJSONPath:
_, _ = w.Write([]byte(`{"code":210202,"msg":"forbidden","request_id":"import-rid","data":{"reason":"forbidden"}}`))
default:
t.Fatalf("unexpected path: %s", r.URL.Path)
}
}))
defer srv.Close()
d := newTestDialog(srv.URL)
_, err := d.ImportJSON(&ImportJSONRequest{})
if err == nil {
t.Fatal("ImportJSON should return error")
}
apiErr, ok := err.(*APIError)
if !ok {
t.Fatalf("ImportJSON error should be *APIError but %T", err)
}
if apiErr.Code != 210202 || apiErr.Msg != "forbidden" || apiErr.RequestID != testImportRequest {
t.Fatalf("bad api error: %+v", apiErr)
}
if string(apiErr.Data) != `{"reason":"forbidden"}` {
t.Fatalf("bad api error data: %s", apiErr.Data)
}
}
func TestQueryPlainJSONError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case tokenPath:
_, _ = w.Write([]byte(accessTokenResponse(testTokenRequestID)))
case queryPath:
_, _ = w.Write([]byte(`{"code":110002,"msg":"bad param","request_id":"query-rid"}`))
default:
t.Fatalf("unexpected path: %s", r.URL.Path)
}
}))
defer srv.Close()
d := newTestDialog(srv.URL)
_, err := d.Query(&QueryRequest{Query: testQuery})
if err == nil {
t.Fatal("Query should return error")
}
apiErr, ok := err.(*APIError)
if !ok {
t.Fatalf("Query error should be *APIError but %T", err)
}
if apiErr.Code != 110002 || apiErr.RequestID != testQueryRequestID {
t.Fatalf("bad api error: %+v", apiErr)
}
}
func newDialogAPITestServer(t *testing.T) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body := readBody(t, r)
assertSign(t, r, testToken, body)
switch r.URL.Path {
case tokenPath:
handleDialogToken(t, w, r)
case importJSONPath:
handleDialogImport(t, w, r, body)
case publishPath:
handleDialogPublish(t, w, r, body)
case effectiveProgressPath:
assertToken(t, r)
_, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"progress-rid","data":{"end_time":"","progress":100,"status":1}}`))
case fetchAsyncPath:
assertToken(t, r)
_, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"fetch-rid","data":{"state":2,"msg":"","progress":100,"start":1,"end":2,"url":"","success_skill_info":[{"id":1,"name":"AAA","intents":[{"id":2,"name":"BBB"}]}]}}`))
case queryPath:
handleDialogQuery(t, w, r, body)
default:
t.Fatalf("unexpected path: %s", r.URL.Path)
}
}))
}
func handleDialogToken(t *testing.T, w io.Writer, r *http.Request) {
t.Helper()
if r.Header.Get("X-APPID") != testAppID {
t.Fatalf("bad X-APPID: %s", r.Header.Get("X-APPID"))
}
_, _ = w.Write([]byte(accessTokenResponse(testTokenRequestID)))
}
func handleDialogImport(t *testing.T, w io.Writer, r *http.Request, body []byte) {
t.Helper()
assertToken(t, r)
var req ImportJSONRequest
if err := json.Unmarshal(body, &req); err != nil {
t.Fatalf("bad import body: %v", err)
}
if len(req.Data) != 1 || req.Data[0].Skill != "pre-sale" {
t.Fatalf("bad import request: %+v", req)
}
_, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"import-rid","data":{"task_id":"task-import"}}`))
}
func handleDialogPublish(t *testing.T, w io.Writer, r *http.Request, body []byte) {
t.Helper()
assertToken(t, r)
if len(body) != 0 {
t.Fatalf("publish body should be empty: %s", body)
}
_, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"publish-rid","data":{"task_id":"task-publish"}}`))
}
func handleDialogQuery(t *testing.T, w io.Writer, r *http.Request, body []byte) {
t.Helper()
assertToken(t, r)
if !strings.HasPrefix(r.Header.Get("Content-Type"), "text/plain") {
t.Fatalf("bad content type: %s", r.Header.Get("Content-Type"))
}
plain, err := encryptor.Decrypt(testAESKey, string(body))
if err != nil {
t.Fatalf("decrypt query body error: %v", err)
}
var req QueryRequest
if err = json.Unmarshal(plain, &req); err != nil {
t.Fatalf("bad query body: %v", err)
}
if req.Query != testQuery {
t.Fatalf("bad query: %+v", req)
}
_, _ = w.Write([]byte(encryptQueryResponse(t)))
}
func assertDialogImportJSON(t *testing.T, d *Dialog) {
t.Helper()
res, err := d.ImportJSON(&ImportJSONRequest{
Mode: 0,
Data: []BotIntent{{
Skill: "pre-sale",
Intent: "business-hours",
Disable: false,
Questions: []string{"when are you open"},
Answers: []string{"9:00-18:00"},
}},
})
if err != nil || res.TaskID != testImportTaskID || res.RequestID != testImportRequest {
t.Fatalf("ImportJSON = %+v, %v", res, err)
}
}
func assertDialogPublish(t *testing.T, d *Dialog) {
t.Helper()
res, err := d.Publish()
if err != nil || res.TaskID != testPublishTaskID {
t.Fatalf("Publish = %+v, %v", res, err)
}
}
func assertDialogProgress(t *testing.T, d *Dialog) {
t.Helper()
res, err := d.GetEffectiveProgress(&EffectiveProgressRequest{Env: "online"})
if err != nil || res.Progress != 100 {
t.Fatalf("GetEffectiveProgress = %+v, %v", res, err)
}
}
func assertDialogFetchAsync(t *testing.T, d *Dialog) {
t.Helper()
res, err := d.FetchAsync(&FetchAsyncRequest{TaskID: testImportTaskID})
if err != nil || res.State != 2 || len(res.SuccessSkillInfo) != 1 {
t.Fatalf("FetchAsync = %+v, %v", res, err)
}
}
func assertDialogQuery(t *testing.T, d *Dialog) {
t.Helper()
res, err := d.Query(&QueryRequest{Query: testQuery, Env: "online"})
if err != nil || res.Answer != testQueryAnswer || res.RequestID != testQueryRequestID {
t.Fatalf("Query = %+v, %v", res, err)
}
}
func newTestDialog(baseURL string) *Dialog {
cfg := testDialogConfig(baseURL)
return NewDialog(&aispeechContext.Context{
Config: cfg,
AccessTokenContextHandle: NewAccessToken(cfg),
})
}
func testDialogConfig(baseURL string) *config.Config {
return &config.Config{
AppID: testAppID,
Token: testToken,
AESKey: testAESKey,
Account: testAccount,
BaseURL: baseURL,
Cache: cache.NewMemory(),
}
}
func accountAccessTokenResponse(account string) string {
return `{"code":0,"msg":"success","request_id":"rid","data":{"access_token":"` + account + `-token"}}`
}
func accessTokenResponse(requestID string) string {
return `{"code":0,"msg":"success","request_id":"` + requestID + `","data":{"access_token":"` + testAccessToken + `"}}`
}
func encryptQueryResponse(t *testing.T) string {
t.Helper()
cipherText, err := encryptor.Encrypt(testAESKey, []byte(queryResponse()))
if err != nil {
t.Fatalf("encrypt response error: %v", err)
}
return cipherText
}
func queryResponse() string {
return `{"code":0,"msg":"success","request_id":"query-rid","data":{"answer":"hello answer","answer_type":"text","skill_name":"skill","intent_name":"intent","msg_id":"msg","status":"FAQ","slots":[{"name":"n","value":"v","norm":"v"}]}}`
}
func readBody(t *testing.T, r *http.Request) []byte {
t.Helper()
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("read body error: %v", err)
}
return body
}
func assertToken(t *testing.T, r *http.Request) {
t.Helper()
if r.Header.Get("X-OPENAI-TOKEN") != testAccessToken {
t.Fatalf("bad X-OPENAI-TOKEN: %s", r.Header.Get("X-OPENAI-TOKEN"))
}
}
func assertSign(t *testing.T, r *http.Request, token string, body []byte) {
t.Helper()
timestamp, err := strconv.ParseInt(r.Header.Get("timestamp"), 10, 64)
if err != nil {
t.Fatalf("bad timestamp: %v", err)
}
want := encryptor.Sign(token, timestamp, r.Header.Get("nonce"), body)
if got := r.Header.Get("sign"); got != want {
t.Fatalf("bad sign: got %s want %s body %s", got, want, body)
}
}
+186
View File
@@ -0,0 +1,186 @@
package dialog
import (
"os"
"testing"
"time"
"github.com/silenceper/wechat/v2/aispeech/config"
aispeechContext "github.com/silenceper/wechat/v2/aispeech/context"
"github.com/silenceper/wechat/v2/cache"
)
func TestIntegrationAccessTokenAndQuery(t *testing.T) {
if os.Getenv("AISPEECH_INTEGRATION") != "1" {
t.Skip("set AISPEECH_INTEGRATION=1 to run real aispeech API test")
}
cfg := &config.Config{
AppID: os.Getenv("AISPEECH_APPID"),
Token: os.Getenv("AISPEECH_TOKEN"),
AESKey: os.Getenv("AISPEECH_AES_KEY"),
Cache: cache.NewMemory(),
}
if cfg.AppID == "" || cfg.Token == "" || cfg.AESKey == "" {
t.Fatal("AISPEECH_APPID, AISPEECH_TOKEN and AISPEECH_AES_KEY are required")
}
d := NewDialog(&aispeechContext.Context{
Config: cfg,
AccessTokenContextHandle: NewAccessToken(cfg),
})
accessToken, err := d.GetAccessToken()
if err != nil {
t.Fatalf("GetAccessToken error: %v", err)
}
if accessToken == "" {
t.Fatal("access token is empty")
}
res, err := d.Query(&QueryRequest{
Query: "你好",
Env: "online",
UserID: "gowechat-integration-test",
})
if err != nil {
t.Fatalf("Query error: %v", err)
}
if res.RequestID == "" {
t.Fatal("query request_id is empty")
}
}
func TestIntegrationFullDialogFlow(t *testing.T) {
if os.Getenv("AISPEECH_INTEGRATION_MUTATION") != "1" {
t.Skip("set AISPEECH_INTEGRATION_MUTATION=1 to run import/publish/query real aispeech API test")
}
d := newIntegrationDialog(t)
now := time.Now().Unix()
question := "codex-smoke-test-question-" + time.Unix(now, 0).Format("20060102150405")
answer := "codex-smoke-test-answer-" + time.Unix(now, 0).Format("20060102150405")
importRes, err := d.ImportJSON(&ImportJSONRequest{
Mode: 0,
Data: []BotIntent{{
Skill: "CodexSmokeTest",
Intent: question,
Disable: false,
Questions: []string{question},
Answers: []string{answer},
}},
})
if err != nil {
t.Fatalf("ImportJSON error: %v", err)
}
if importRes.TaskID == "" {
t.Fatal("ImportJSON task_id is empty")
}
t.Logf("ImportJSON request_id=%s task_id=%s", importRes.RequestID, importRes.TaskID)
importTask := waitAsyncTask(t, d, importRes.TaskID, 2*time.Minute)
if importTask.State != 2 {
t.Fatalf("import task failed: state=%d msg=%s progress=%d", importTask.State, importTask.Msg, importTask.Progress)
}
t.Logf("FetchAsync import request_id=%s state=%d progress=%d", importTask.RequestID, importTask.State, importTask.Progress)
publishRes, err := d.Publish()
if err != nil {
t.Fatalf("Publish error: %v", err)
}
if publishRes.TaskID == "" {
t.Fatal("Publish task_id is empty")
}
t.Logf("Publish request_id=%s task_id=%s", publishRes.RequestID, publishRes.TaskID)
progress := waitEffectiveProgress(t, d, "online", 3*time.Minute)
if progress.Status != 1 {
t.Fatalf("publish progress failed: status=%d progress=%d", progress.Status, progress.Progress)
}
t.Logf("GetEffectiveProgress request_id=%s status=%d progress=%d", progress.RequestID, progress.Status, progress.Progress)
queryRes := waitQueryAnswer(t, d, question, answer, 2*time.Minute)
t.Logf("Query request_id=%s status=%s answer_type=%s answer=%s", queryRes.RequestID, queryRes.Status, queryRes.AnswerType, queryRes.Answer)
}
func newIntegrationDialog(t *testing.T) *Dialog {
t.Helper()
cfg := &config.Config{
AppID: os.Getenv("AISPEECH_APPID"),
Token: os.Getenv("AISPEECH_TOKEN"),
AESKey: os.Getenv("AISPEECH_AES_KEY"),
Account: os.Getenv("AISPEECH_ACCOUNT"),
Cache: cache.NewMemory(),
}
if cfg.AppID == "" || cfg.Token == "" || cfg.AESKey == "" {
t.Fatal("AISPEECH_APPID, AISPEECH_TOKEN and AISPEECH_AES_KEY are required")
}
return NewDialog(&aispeechContext.Context{
Config: cfg,
AccessTokenContextHandle: NewAccessToken(cfg),
})
}
func waitAsyncTask(t *testing.T, d *Dialog, taskID string, timeout time.Duration) *FetchAsyncResponse {
t.Helper()
deadline := time.Now().Add(timeout)
for {
res, err := d.FetchAsync(&FetchAsyncRequest{TaskID: taskID})
if err != nil {
t.Fatalf("FetchAsync error: %v", err)
}
if res.State == 2 || res.State == 3 {
return res
}
if time.Now().After(deadline) {
t.Fatalf("FetchAsync timeout: state=%d progress=%d msg=%s", res.State, res.Progress, res.Msg)
}
time.Sleep(3 * time.Second)
}
}
func waitEffectiveProgress(t *testing.T, d *Dialog, env string, timeout time.Duration) *EffectiveProgressResponse {
t.Helper()
deadline := time.Now().Add(timeout)
for {
res, err := d.GetEffectiveProgress(&EffectiveProgressRequest{Env: env})
if err != nil {
t.Fatalf("GetEffectiveProgress error: %v", err)
}
if res.Status == 1 || res.Status == 2 {
return res
}
if time.Now().After(deadline) {
t.Fatalf("GetEffectiveProgress timeout: status=%d progress=%d", res.Status, res.Progress)
}
time.Sleep(5 * time.Second)
}
}
func waitQueryAnswer(t *testing.T, d *Dialog, question, answer string, timeout time.Duration) *QueryResponse {
t.Helper()
deadline := time.Now().Add(timeout)
for {
res, err := d.Query(&QueryRequest{
Query: question,
Env: "online",
UserID: "gowechat-full-integration-test",
})
if err != nil {
t.Fatalf("Query error: %v", err)
}
if res.Answer == answer {
return res
}
if time.Now().After(deadline) {
t.Fatalf("Query timeout: status=%s answer=%s want=%s", res.Status, res.Answer, answer)
}
time.Sleep(5 * time.Second)
}
}
+48
View File
@@ -0,0 +1,48 @@
package dialog
// AccessTokenRequest 获取 access token 请求.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/token.html
type AccessTokenRequest struct {
Account string `json:"account,omitempty"`
}
// ImportJSONRequest 简单问答导入请求.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/import.html
type ImportJSONRequest struct {
Mode int `json:"mode"`
Data []BotIntent `json:"data"`
}
// BotIntent 简单问答意图.
type BotIntent struct {
Skill string `json:"skill"`
Intent string `json:"intent"`
Threshold string `json:"threshold,omitempty"`
Disable bool `json:"disable"`
Questions []string `json:"questions"`
Answers []string `json:"answers"`
}
// EffectiveProgressRequest 发布进度查询请求.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/progress.html
type EffectiveProgressRequest struct {
Env string `json:"env"`
}
// FetchAsyncRequest 异步任务查询请求.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/fetch.html
type FetchAsyncRequest struct {
TaskID string `json:"task_id"`
}
// QueryRequest 调用智能对话请求.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/query.html
type QueryRequest struct {
Query string `json:"query"`
Env string `json:"env,omitempty"`
FirstPrioritySkills []string `json:"first_priority_skills,omitempty"`
SecondPrioritySkills []string `json:"second_priority_skills,omitempty"`
UserName string `json:"user_name,omitempty"`
Avatar string `json:"avatar,omitempty"`
UserID string `json:"userid,omitempty"`
}
+137
View File
@@ -0,0 +1,137 @@
package dialog
import (
"encoding/json"
"fmt"
)
type apiResponse struct {
RequestID string `json:"request_id"`
Code int64 `json:"code"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
}
// APIError 智能对话接口错误.
type APIError struct {
APIName string
Code int64
Msg string
RequestID string
Data json.RawMessage
}
func (e *APIError) Error() string {
return fmt.Sprintf("%s Error , code=%d , msg=%s , request_id=%s", e.APIName, e.Code, e.Msg, e.RequestID)
}
type accessTokenData struct {
AccessToken string `json:"access_token"`
}
// ImportJSONResponse 简单问答导入响应.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/import.html
type ImportJSONResponse struct {
RequestID string `json:"-"`
TaskID string `json:"task_id"`
}
// PublishResponse 发布响应.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/publish.html
type PublishResponse struct {
RequestID string `json:"-"`
TaskID string `json:"task_id"`
}
// EffectiveProgressResponse 发布进度查询响应.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/progress.html
type EffectiveProgressResponse struct {
RequestID string `json:"-"`
EndTime string `json:"end_time"`
Progress int `json:"progress"`
Status int `json:"status"`
}
// FetchAsyncResponse 异步任务查询响应.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/fetch.html
type FetchAsyncResponse struct {
RequestID string `json:"-"`
State int `json:"state"`
Msg string `json:"msg"`
Progress int `json:"progress"`
Start int64 `json:"start"`
End int64 `json:"end"`
URL string `json:"url"`
TotalCount int `json:"total_count"`
SuccessCount int `json:"success_count"`
FailCount int `json:"fail_count"`
ReplaceCount int `json:"replace_count"`
SuccessUploadCount int `json:"success_upload_count"`
SuccessSkillInfo []SkillInfo `json:"success_skill_info"`
FailSkillInfo []SkillInfo `json:"fail_skill_info"`
}
// SkillInfo 技能信息.
type SkillInfo struct {
ID int64 `json:"id"`
Name string `json:"name"`
Intents []IntentInfo `json:"intents"`
}
// IntentInfo 意图信息.
type IntentInfo struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
// QueryResponse 调用智能对话响应.
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/query.html
type QueryResponse struct {
RequestID string `json:"-"`
Answer string `json:"answer"`
RawAnswer json.RawMessage `json:"-"`
AnswerType string `json:"answer_type"`
SkillName string `json:"skill_name"`
IntentName string `json:"intent_name"`
MsgID string `json:"msg_id"`
Options []Option `json:"options"`
Status string `json:"status"`
Slots []SlotDetail `json:"slots"`
}
// Option 推荐问题.
type Option struct {
AnsNodeName string `json:"ans_node_name"`
Title string `json:"title"`
Answer string `json:"answer"`
Confidence float64 `json:"confidence"`
}
// SlotDetail 槽位数据.
type SlotDetail struct {
Name string `json:"name"`
Value string `json:"value"`
Norm string `json:"norm"`
}
func decodeResponse(response []byte, data interface{}, apiName string) (string, error) {
var res apiResponse
if err := json.Unmarshal(response, &res); err != nil {
return "", fmt.Errorf("json Unmarshal Error, err=%v", err)
}
if res.Code != 0 {
return "", &APIError{
APIName: apiName,
Code: res.Code,
Msg: res.Msg,
RequestID: res.RequestID,
Data: res.Data,
}
}
if data != nil && len(res.Data) > 0 && string(res.Data) != "null" {
if err := json.Unmarshal(res.Data, data); err != nil {
return "", fmt.Errorf("json Unmarshal Data Error, err=%v", err)
}
}
return res.RequestID, nil
}
+89
View File
@@ -0,0 +1,89 @@
package dialog
import (
stdcontext "context"
"encoding/json"
"errors"
"fmt"
"sync"
"time"
"github.com/silenceper/wechat/v2/aispeech/config"
"github.com/silenceper/wechat/v2/cache"
)
const accessTokenCacheKeyPrefix = "gowechat_aispeech_"
var errEmptyAccessToken = errors.New("aispeech access_token is empty")
// AccessToken 微信智能对话 access token.
type AccessToken struct {
cfg *config.Config
accessTokenLock *sync.Mutex
}
// NewAccessToken new AccessToken.
func NewAccessToken(cfg *config.Config) *AccessToken {
if cfg.Cache == nil {
panic("cache is needed")
}
return &AccessToken{
cfg: cfg,
accessTokenLock: new(sync.Mutex),
}
}
// GetAccessToken 获取 access token.
func (ak *AccessToken) GetAccessToken() (string, error) {
return ak.GetAccessTokenContext(stdcontext.Background())
}
// GetAccessTokenContext 获取 access token.
func (ak *AccessToken) GetAccessTokenContext(ctx stdcontext.Context) (string, error) {
cacheKey := fmt.Sprintf("%s_access_token_%s_%s", accessTokenCacheKeyPrefix, ak.cfg.AppID, ak.cfg.Account)
if val := cache.GetContext(ctx, ak.cfg.Cache, cacheKey); val != nil {
if accessToken, ok := val.(string); ok && accessToken != "" {
return accessToken, nil
}
}
ak.accessTokenLock.Lock()
defer ak.accessTokenLock.Unlock()
if val := cache.GetContext(ctx, ak.cfg.Cache, cacheKey); val != nil {
if accessToken, ok := val.(string); ok && accessToken != "" {
return accessToken, nil
}
}
accessToken, err := ak.getAccessTokenFromServer(ctx)
if err != nil {
return "", err
}
if err = cache.SetContext(ctx, ak.cfg.Cache, cacheKey, accessToken, 110*time.Minute); err != nil {
return "", err
}
return accessToken, nil
}
func (ak *AccessToken) getAccessTokenFromServer(ctx stdcontext.Context) (string, error) {
req := &AccessTokenRequest{Account: ak.cfg.Account}
body, err := json.Marshal(req)
if err != nil {
return "", err
}
response, err := post(ctx, ak.cfg, tokenPath, body, "application/json", "", ak.cfg.AppID)
if err != nil {
return "", err
}
var res accessTokenData
if _, err = decodeResponse(response, &res, "AISpeechGetAccessToken"); err != nil {
return "", err
}
if res.AccessToken == "" {
return "", errEmptyAccessToken
}
return res.AccessToken, nil
}
+100
View File
@@ -0,0 +1,100 @@
package encryptor
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
"fmt"
)
var (
// ErrInvalidBlockSize block size 不合法.
ErrInvalidBlockSize = errors.New("invalid block size")
// ErrInvalidPKCS7Data PKCS7 数据不合法.
ErrInvalidPKCS7Data = errors.New("invalid PKCS7 data")
// ErrInvalidPKCS7Padding padding 不合法.
ErrInvalidPKCS7Padding = errors.New("invalid PKCS7 padding")
)
// Encrypt encrypts plaintext with AES-CBC and returns base64 ciphertext.
func Encrypt(aesKey string, plaintext []byte) (string, error) {
key, err := decodeAESKey(aesKey)
if err != nil {
return "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
plaintext = pkcs7Pad(plaintext, block.BlockSize())
ciphertext := make([]byte, len(plaintext))
mode := cipher.NewCBCEncrypter(block, key[:aes.BlockSize])
mode.CryptBlocks(ciphertext, plaintext)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt decrypts base64 AES-CBC ciphertext.
func Decrypt(aesKey, ciphertext string) ([]byte, error) {
key, err := decodeAESKey(aesKey)
if err != nil {
return nil, err
}
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return nil, err
}
if len(data) == 0 || len(data)%aes.BlockSize != 0 {
return nil, ErrInvalidPKCS7Data
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
plaintext := make([]byte, len(data))
mode := cipher.NewCBCDecrypter(block, key[:aes.BlockSize])
mode.CryptBlocks(plaintext, data)
return pkcs7Unpad(plaintext, block.BlockSize())
}
func decodeAESKey(encodingAESKey string) ([]byte, error) {
key, err := base64.StdEncoding.DecodeString(encodingAESKey + "=")
if err != nil {
return nil, err
}
if len(key) != 32 {
return nil, fmt.Errorf("encodingAESKey invalid")
}
return key, nil
}
func pkcs7Pad(data []byte, blockSize int) []byte {
padding := blockSize - len(data)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(data, padText...)
}
func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
if blockSize <= 0 {
return nil, ErrInvalidBlockSize
}
if len(data) == 0 || len(data)%blockSize != 0 {
return nil, ErrInvalidPKCS7Data
}
padding := int(data[len(data)-1])
if padding == 0 || padding > blockSize || padding > len(data) {
return nil, ErrInvalidPKCS7Padding
}
for i := len(data) - padding; i < len(data); i++ {
if int(data[i]) != padding {
return nil, ErrInvalidPKCS7Padding
}
}
return data[:len(data)-padding], nil
}
+33
View File
@@ -0,0 +1,33 @@
package encryptor
import "testing"
func TestSign(t *testing.T) {
sign := Sign("YV78Pyj1VvqdNGpMJ1pHic0bIBOWMv", 1711001766, "abc", nil)
if sign != "fff8dae1356e7867ea98743439f0e9f8" {
t.Fatalf("bad sign: %s", sign)
}
}
func TestEncryptDecrypt(t *testing.T) {
aesKey := "q1Os1ZMe0nG28KUEx9lg3HjK7V5QyXvi212fzsgDqgz"
plainText := []byte(`{"query":"hello"}`)
cipherText, err := Encrypt(aesKey, plainText)
if err != nil {
t.Fatalf("Encrypt error: %v", err)
}
got, err := Decrypt(aesKey, cipherText)
if err != nil {
t.Fatalf("Decrypt error: %v", err)
}
if string(got) != string(plainText) {
t.Fatalf("bad plaintext: %s", got)
}
}
func TestDecryptInvalidKey(t *testing.T) {
if _, err := Encrypt("bad-key", []byte("hello")); err == nil {
t.Fatal("Encrypt should return error")
}
}
+20
View File
@@ -0,0 +1,20 @@
package encryptor
import (
"crypto/md5"
"encoding/hex"
"strconv"
)
// Sign calculates the aispeech request signature.
func Sign(token string, timestamp int64, nonce string, body []byte) string {
bodyMD5 := md5.Sum(body)
bodySign := hex.EncodeToString(bodyMD5[:])
h := md5.New()
h.Write([]byte(token))
h.Write([]byte(strconv.FormatInt(timestamp, 10)))
h.Write([]byte(nonce))
h.Write([]byte(bodySign))
return hex.EncodeToString(h.Sum(nil))
}
+43 -27
View File
@@ -5,53 +5,64 @@ import (
"time"
)
// Memory struct contains *memcache.Client
// Memory is an concurrent-safe in-process cache backed by a plain map.
type Memory struct {
sync.Mutex
mu sync.RWMutex
data map[string]*data
}
// data holds a single cached value together with its expiry time.
type data struct {
Data interface{}
Expired time.Time
}
// NewMemory create new memcache
// NewMemory returns a ready-to-use in-memory cache.
func NewMemory() *Memory {
return &Memory{
data: map[string]*data{},
}
}
// Get return cached value
// Get returns the cached value for key.
// Returns nil if the key does not exist or has expired.
func (mem *Memory) Get(key string) interface{} {
if ret, ok := mem.data[key]; ok {
if ret.Expired.Before(time.Now()) {
mem.deleteKey(key)
return nil
}
return ret.Data
mem.mu.RLock()
ret, ok := mem.data[key]
expired := ok && ret.Expired.Before(time.Now())
mem.mu.RUnlock()
if !ok {
return nil
}
return nil
if expired {
mem.deleteExpiredKey(key)
return nil
}
return ret.Data
}
// IsExist check value exists in memcache.
func (mem *Memory) IsExist(key string) bool {
if ret, ok := mem.data[key]; ok {
if ret.Expired.Before(time.Now()) {
mem.deleteKey(key)
return false
}
return true
mem.mu.RLock()
ret, ok := mem.data[key]
expired := ok && ret.Expired.Before(time.Now())
mem.mu.RUnlock()
if !ok {
return false
}
return false
if expired {
mem.deleteExpiredKey(key)
return false
}
return true
}
// Set cached value with key and expire time.
func (mem *Memory) Set(key string, val interface{}, timeout time.Duration) (err error) {
mem.Lock()
defer mem.Unlock()
mem.mu.Lock()
defer mem.mu.Unlock()
mem.data[key] = &data{
Data: val,
@@ -62,13 +73,18 @@ func (mem *Memory) Set(key string, val interface{}, timeout time.Duration) (err
// Delete delete value in memcache.
func (mem *Memory) Delete(key string) error {
mem.deleteKey(key)
mem.mu.Lock()
defer mem.mu.Unlock()
delete(mem.data, key)
return nil
}
// deleteKey
func (mem *Memory) deleteKey(key string) {
mem.Lock()
defer mem.Unlock()
delete(mem.data, key)
// deleteExpiredKey deletes a key only if it has expired.
// Caller must NOT hold any lock.
func (mem *Memory) deleteExpiredKey(key string) {
mem.mu.Lock()
defer mem.mu.Unlock()
if d, ok := mem.data[key]; ok && d.Expired.Before(time.Now()) {
delete(mem.data, key)
}
}
+116
View File
@@ -0,0 +1,116 @@
// 运行测试:go test -race -v ./cache/ -run "TestMemory" -count=1
package cache
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestMemoryGet(t *testing.T) {
mem := NewMemory()
err := mem.Set("username", "silenceper", 10*time.Second)
assert.NoError(t, err)
val := mem.Get("username")
assert.Equal(t, "silenceper", val)
val = mem.Get("unknown-key")
assert.Nil(t, val)
}
func TestMemoryIsExist(t *testing.T) {
mem := NewMemory()
err := mem.Set("username", "silenceper", 10*time.Second)
assert.NoError(t, err)
assert.True(t, mem.IsExist("username"))
assert.False(t, mem.IsExist("unknown-key"))
}
func TestMemoryDelete(t *testing.T) {
mem := NewMemory()
err := mem.Set("username", "silenceper", 10*time.Second)
assert.NoError(t, err)
err = mem.Delete("username")
assert.NoError(t, err)
// delete 不存在的 key 不应报错
err = mem.Delete("unknown-key")
assert.NoError(t, err)
}
func TestMemoryExpire(t *testing.T) {
mem := NewMemory()
err := mem.Set("username", "silenceper", 10*time.Millisecond)
assert.NoError(t, err)
assert.True(t, mem.IsExist("username"))
time.Sleep(20 * time.Millisecond)
assert.False(t, mem.IsExist("username"))
assert.Nil(t, mem.Get("username"))
}
// TestMemoryConcurrentOps 验证 Get/IsExist/Set/Delete 并发执行不产生数据竞争
func TestMemoryConcurrentOps(t *testing.T) {
mem := NewMemory()
_ = mem.Set("key", "value", time.Minute)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(4)
// 并发读
go func() {
defer wg.Done()
_ = mem.Get("key")
}()
// 并发 IsExist
go func() {
defer wg.Done()
_ = mem.IsExist("key")
}()
// 并发写
go func(i int) {
defer wg.Done()
_ = mem.Set("key", i, time.Minute)
}(i)
// 并发删除
go func() {
defer wg.Done()
_ = mem.Delete("key")
}()
}
wg.Wait()
}
// TestMemoryConcurrentExpireAndRead 验证过期惰性删除在并发场景下不会死锁
func TestMemoryConcurrentExpireAndRead(t *testing.T) {
mem := NewMemory()
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(2)
go func(i int) {
defer wg.Done()
key := "expire-key"
_ = mem.Set(key, i, 5*time.Millisecond)
}(i)
go func() {
defer wg.Done()
time.Sleep(3 * time.Millisecond)
// 此时 key 可能已过期,触发 deleteKey,验证不死锁
_ = mem.Get("expire-key")
_ = mem.IsExist("expire-key")
}()
}
wg.Wait()
}
+64 -11
View File
@@ -2,6 +2,8 @@ package cache
import (
"context"
"crypto/tls"
"net"
"time"
"github.com/go-redis/redis/v8"
@@ -15,25 +17,76 @@ 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" yaml:"host"`
Username string `json:"username" yaml:"username"`
Password string `json:"password" yaml:"password"`
Database int `json:"database" yaml:"database"`
MinIdleConns int `json:"min_idle_conns" yaml:"min_idle_conns"` // 最小空闲连接数
PoolSize int `json:"pool_size" yaml:"pool_size"` // 连接池大小,0 表示使用默认值(即 CPU 核心数 * 10)
MaxRetries int `json:"max_retries" yaml:"max_retries"` // 最大重试次数,-1 表示不重试,0 表示使用默认值(即 3 次)
DialTimeout int `json:"dial_timeout" yaml:"dial_timeout"` // 连接超时时间(秒),0 表示使用默认值(即 5 秒)
ReadTimeout int `json:"read_timeout" yaml:"read_timeout"` // 读取超时时间(秒),-1 表示不超时,0 表示使用默认值(即 3 秒)
WriteTimeout int `json:"write_timeout" yaml:"write_timeout"` // 写入超时时间(秒),-1 表示不超时,0 表示使用默认值(即 ReadTimeout)
PoolTimeout int `json:"pool_timeout" yaml:"pool_timeout"` // 连接池获取连接超时时间(秒),0 表示使用默认值(即 ReadTimeout + 1 秒)
IdleTimeout int `json:"idle_timeout" yaml:"idle_timeout"` // 空闲连接超时时间(秒),-1 表示禁用空闲连接超时检查,0 表示使用默认值(即 5 分钟)
UseTLS bool `json:"use_tls" yaml:"use_tls"` // 是否使用 TLS
// Deprecated: 应使用 MinIdleConns 代替
MaxIdle int `json:"max_idle" yaml:"max_idle"`
// Deprecated: 应使用 PoolSize 代替
MaxActive int `json:"max_active" yaml:"max_active"`
}
// 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,
})
MinIdleConns: opts.MinIdleConns,
PoolSize: opts.PoolSize,
MaxRetries: opts.MaxRetries,
}
// 兼容旧的 MaxIdle 参数,仅在未显式设置 MinIdleConns 时生效
if opts.MaxIdle > 0 && opts.MinIdleConns == 0 {
uniOpt.MinIdleConns = opts.MaxIdle
}
// 兼容旧的 MaxActive 参数,仅在未显式设置 PoolSize 时生效
if opts.MaxActive > 0 && opts.PoolSize == 0 {
uniOpt.PoolSize = opts.MaxActive
}
applyTimeout := func(seconds int, target *time.Duration) {
if seconds > 0 {
*target = time.Duration(seconds) * time.Second
} else if seconds == -1 {
// 当 seconds 为 -1 时,表示禁用超时:按 go-redis 约定,将超时时间设置为负值(如 -1ns)代表「无超时」
*target = -1
}
// 当 seconds 为 0 时,使用 go-redis 的默认超时配置:
// 不修改 target,保持其零值(0),由 go-redis 解释为“使用默认值”
}
applyTimeout(opts.DialTimeout, &uniOpt.DialTimeout)
applyTimeout(opts.ReadTimeout, &uniOpt.ReadTimeout)
applyTimeout(opts.WriteTimeout, &uniOpt.WriteTimeout)
applyTimeout(opts.PoolTimeout, &uniOpt.PoolTimeout)
applyTimeout(opts.IdleTimeout, &uniOpt.IdleTimeout)
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}
}
+267 -1
View File
@@ -6,6 +6,7 @@ import (
"time"
"github.com/alicebob/miniredis/v2"
"github.com/go-redis/redis/v8"
)
func TestRedis(t *testing.T) {
@@ -18,7 +19,16 @@ func TestRedis(t *testing.T) {
timeoutDuration = time.Second
ctx = context.Background()
opts = &RedisOpts{
Host: server.Addr(),
Host: server.Addr(),
Password: "",
Database: 0,
PoolSize: 10,
MinIdleConns: 5,
DialTimeout: 5,
ReadTimeout: 5,
WriteTimeout: 5,
PoolTimeout: 5,
IdleTimeout: 300,
}
redis = NewRedis(ctx, opts)
val = "silenceper"
@@ -44,3 +54,259 @@ func TestRedis(t *testing.T) {
t.Errorf("delete Error , err=%v", err)
}
}
// setupRedisServer 创建并返回一个 miniredis 服务器实例
func setupRedisServer(t *testing.T) *miniredis.Miniredis {
server, err := miniredis.Run()
if err != nil {
t.Fatal("miniredis.Run Error", err)
}
t.Cleanup(server.Close)
return server
}
// TestRedisMaxIdleMapping 测试只设置MaxIdle应该映射到MinIdleConns
func TestRedisMaxIdleMapping(t *testing.T) {
server := setupRedisServer(t)
ctx := context.Background()
opts := &RedisOpts{
Host: server.Addr(),
Database: 0,
MaxIdle: 10,
}
r := NewRedis(ctx, opts)
// 获取底层的 UniversalClient 并断言为 *redis.Client
client, ok := r.conn.(*redis.Client)
if !ok {
t.Fatal("无法转换为 *redis.Client")
}
// 注意:MinIdleConns 表示期望的最小空闲连接数,但实际空闲连接数可能不同
// 我们需要通过 Options() 来验证配置是否正确应用
clientOpts := client.Options()
if clientOpts.MinIdleConns != 10 {
t.Errorf("期望 MinIdleConns = 10, 实际 = %d", clientOpts.MinIdleConns)
}
}
// TestRedisMaxActiveMapping 测试只设置MaxActive应该映射到PoolSize
func TestRedisMaxActiveMapping(t *testing.T) {
server := setupRedisServer(t)
ctx := context.Background()
opts := &RedisOpts{
Host: server.Addr(),
Database: 0,
MaxActive: 20,
}
r := NewRedis(ctx, opts)
client, ok := r.conn.(*redis.Client)
if !ok {
t.Fatal("无法转换为 *redis.Client")
}
clientOpts := client.Options()
if clientOpts.PoolSize != 20 {
t.Errorf("期望 PoolSize = 20, 实际 = %d", clientOpts.PoolSize)
}
}
// TestRedisNewFieldsPriority 测试新字段应该优先于旧字段
func TestRedisNewFieldsPriority(t *testing.T) {
server := setupRedisServer(t)
ctx := context.Background()
opts := &RedisOpts{
Host: server.Addr(),
Database: 0,
MaxIdle: 5,
MinIdleConns: 15,
MaxActive: 10,
PoolSize: 30,
}
r := NewRedis(ctx, opts)
client, ok := r.conn.(*redis.Client)
if !ok {
t.Fatal("无法转换为 *redis.Client")
}
clientOpts := client.Options()
if clientOpts.MinIdleConns != 15 {
t.Errorf("期望 MinIdleConns = 15 (新字段优先), 实际 = %d", clientOpts.MinIdleConns)
}
if clientOpts.PoolSize != 30 {
t.Errorf("期望 PoolSize = 30 (新字段优先), 实际 = %d", clientOpts.PoolSize)
}
}
// TestRedisPositiveTimeouts 测试正值超时应该正确应用
func TestRedisPositiveTimeouts(t *testing.T) {
server := setupRedisServer(t)
ctx := context.Background()
opts := &RedisOpts{
Host: server.Addr(),
Database: 0,
DialTimeout: 10,
ReadTimeout: 20,
WriteTimeout: 30,
PoolTimeout: 40,
IdleTimeout: 50,
}
r := NewRedis(ctx, opts)
client, ok := r.conn.(*redis.Client)
if !ok {
t.Fatal("无法转换为 *redis.Client")
}
clientOpts := client.Options()
if clientOpts.DialTimeout != 10*time.Second {
t.Errorf("期望 DialTimeout = 10s, 实际 = %v", clientOpts.DialTimeout)
}
if clientOpts.ReadTimeout != 20*time.Second {
t.Errorf("期望 ReadTimeout = 20s, 实际 = %v", clientOpts.ReadTimeout)
}
if clientOpts.WriteTimeout != 30*time.Second {
t.Errorf("期望 WriteTimeout = 30s, 实际 = %v", clientOpts.WriteTimeout)
}
if clientOpts.PoolTimeout != 40*time.Second {
t.Errorf("期望 PoolTimeout = 40s, 实际 = %v", clientOpts.PoolTimeout)
}
if clientOpts.IdleTimeout != 50*time.Second {
t.Errorf("期望 IdleTimeout = 50s, 实际 = %v", clientOpts.IdleTimeout)
}
}
// TestRedisNegativeTimeouts 测试-1值应该禁用超时
func TestRedisNegativeTimeouts(t *testing.T) {
server := setupRedisServer(t)
ctx := context.Background()
opts := &RedisOpts{
Host: server.Addr(),
Database: 0,
DialTimeout: -1,
ReadTimeout: -1,
WriteTimeout: -1,
PoolTimeout: -1,
IdleTimeout: -1,
}
r := NewRedis(ctx, opts)
client, ok := r.conn.(*redis.Client)
if !ok {
t.Fatal("无法转换为 *redis.Client")
}
clientOpts := client.Options()
// -1 应该被设置为负值表示禁用超时
// DialTimeout, PoolTimeout, IdleTimeout 会被设置为 -1ns
if clientOpts.DialTimeout != -1 {
t.Errorf("期望 DialTimeout = -1ns (禁用), 实际 = %v", clientOpts.DialTimeout)
}
// ReadTimeout 和 WriteTimeout 在 go-redis 中有特殊处理
// 当设置为负值时,会被规范化为 0,这也表示无超时
t.Logf("ReadTimeout = %v (设置为-1后的值)", clientOpts.ReadTimeout)
t.Logf("WriteTimeout = %v (设置为-1后的值)", clientOpts.WriteTimeout)
if clientOpts.PoolTimeout != -1 {
t.Errorf("期望 PoolTimeout = -1ns (禁用), 实际 = %v", clientOpts.PoolTimeout)
}
if clientOpts.IdleTimeout != -1 {
t.Errorf("期望 IdleTimeout = -1ns (禁用), 实际 = %v", clientOpts.IdleTimeout)
}
}
// TestRedisZeroTimeouts 测试0值应该使用go-redis默认值
func TestRedisZeroTimeouts(t *testing.T) {
server := setupRedisServer(t)
ctx := context.Background()
opts := &RedisOpts{
Host: server.Addr(),
Database: 0,
DialTimeout: 0,
ReadTimeout: 0,
WriteTimeout: 0,
PoolTimeout: 0,
IdleTimeout: 0,
}
r := NewRedis(ctx, opts)
client, ok := r.conn.(*redis.Client)
if !ok {
t.Fatal("无法转换为 *redis.Client")
}
clientOpts := client.Options()
// 0值应该保持为0,由 go-redis 使用默认值
// go-redis 的默认值:
// DialTimeout: 5s
// ReadTimeout: 3s
// WriteTimeout: ReadTimeout
// PoolTimeout: ReadTimeout + 1s
// IdleTimeout: 5min
if clientOpts.DialTimeout == 0 {
t.Error("期望 DialTimeout 使用 go-redis 默认值 (5s), 实际为 0")
}
if clientOpts.ReadTimeout == 0 {
t.Error("期望 ReadTimeout 使用 go-redis 默认值 (3s), 实际为 0")
}
if clientOpts.WriteTimeout == 0 {
t.Error("期望 WriteTimeout 使用 go-redis 默认值 (ReadTimeout), 实际为 0")
}
if clientOpts.PoolTimeout == 0 {
t.Error("期望 PoolTimeout 使用 go-redis 默认值 (ReadTimeout + 1s), 实际为 0")
}
if clientOpts.IdleTimeout == 0 {
t.Error("期望 IdleTimeout 使用 go-redis 默认值 (5min), 实际为 0")
}
}
// TestRedisMixedTimeouts 测试混合超时配置
func TestRedisMixedTimeouts(t *testing.T) {
server := setupRedisServer(t)
ctx := context.Background()
opts := &RedisOpts{
Host: server.Addr(),
Database: 0,
DialTimeout: 5, // 正值
ReadTimeout: -1, // 禁用
WriteTimeout: 0, // 使用默认值
PoolTimeout: 10, // 正值
IdleTimeout: -1, // 禁用
}
r := NewRedis(ctx, opts)
client, ok := r.conn.(*redis.Client)
if !ok {
t.Fatal("无法转换为 *redis.Client")
}
clientOpts := client.Options()
if clientOpts.DialTimeout != 5*time.Second {
t.Errorf("期望 DialTimeout = 5s, 实际 = %v", clientOpts.DialTimeout)
}
// ReadTimeout 设置为 -1,会被 go-redis 处理为 0(无超时)
t.Logf("ReadTimeout = %v (设置为-1后的值)", clientOpts.ReadTimeout)
// WriteTimeout 设置为 0,应该使用 go-redis 的默认值
// 默认值通常是 ReadTimeout 的值
t.Logf("WriteTimeout = %v (设置为0后使用的默认值)", clientOpts.WriteTimeout)
if clientOpts.PoolTimeout != 10*time.Second {
t.Errorf("期望 PoolTimeout = 10s, 实际 = %v", clientOpts.PoolTimeout)
}
// IdleTimeout 设置为 -1,应该被设置为 -1ns(禁用空闲超时)
if clientOpts.IdleTimeout != -1 {
t.Errorf("期望 IdleTimeout = -1ns (禁用), 实际 = %v", clientOpts.IdleTimeout)
}
}
+21 -6
View File
@@ -1,6 +1,7 @@
package credential
import (
context2 "context"
"encoding/json"
"fmt"
"sync"
@@ -42,10 +43,22 @@ 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 {
return val.(string), nil
if ticketStr = val.(string); ticketStr != "" {
return
}
}
js.jsAPITicketLock.Lock()
@@ -53,11 +66,13 @@ func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err
// 双检,防止重复从微信服务器获取
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
return val.(string), nil
if ticketStr = val.(string); ticketStr != "" {
return
}
}
var ticket ResTicket
ticket, err = GetTicketFromServer(accessToken)
ticket, err = GetTicketFromServerContext(ctx, accessToken)
if err != nil {
return
}
@@ -67,11 +82,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
}
+22
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")
}
+8
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)
}
+10 -1
View File
@@ -1,3 +1,12 @@
# 智能对话
TODO
## 接入智能对话
- 获取 Accesstoken:`POST /v2/token`
- 简单问答导入:`POST /v2/bot/import/json`
- 异步任务查询:`POST /v2/async/fetch`
- 发布机器人:`POST /v2/bot/publish`
- 查询机器人发布进度:`POST /v2/bot/effective_progress`
- 调用智能对话:`POST /v2/bot/query`
官方文档:https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/token.html
+66
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
}
+165 -6
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
}
+295
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
)
+16
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}
}
+618
View File
@@ -0,0 +1,618 @@
package express
import (
"context"
"fmt"
"github.com/silenceper/wechat/v2/util"
)
// 同城配送 API URL
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/intracity_service.html
const (
// 开通门店权限
intracityApplyURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/apply?access_token=%s"
// 创建门店
intracityCreateStoreURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/createstore?access_token=%s"
// 查询门店
intracityQueryStoreURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/querystore?access_token=%s"
// 更新门店
intracityUpdateStoreURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/updatestore?access_token=%s"
// 门店运费充值
intracityStoreChargeURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/storecharge?access_token=%s"
// 门店运费退款
intracityStoreRefundURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/storerefund?access_token=%s"
// 门店运费流水查询
intracityQueryFlowURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/queryflow?access_token=%s"
// 门店余额查询
intracityBalanceQueryURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/balancequery?access_token=%s"
// 预下配送单(查询运费)
intracityPreAddOrderURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/preaddorder?access_token=%s"
// 创建配送单
intracityAddOrderURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/addorder?access_token=%s"
// 查询配送单
intracityQueryOrderURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/queryorder?access_token=%s"
// 取消配送单
intracityCancelOrderURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/cancelorder?access_token=%s"
// 模拟配送回调(仅用于测试)
intracityMockNotifyURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/mocknotify?access_token=%s"
)
// PayMode 充值/扣费主体
type PayMode string
const (
// PayModeStore 门店
PayModeStore PayMode = "PAY_MODE_STORE"
// PayModeApp 小程序
PayModeApp PayMode = "PAY_MODE_APP"
// PayModeComponent 服务商
PayModeComponent PayMode = "PAY_MODE_COMPONENT"
)
// OrderPattern 运力偏好
type OrderPattern uint32
const (
// OrderPatternPriceFirst 价格优先
OrderPatternPriceFirst OrderPattern = 1
// OrderPatternTransFirst 运力优先
OrderPatternTransFirst OrderPattern = 2
)
// FlowType 流水类型
type FlowType uint32
const (
// FlowTypeCharge 充值流水
FlowTypeCharge FlowType = 1
// FlowTypeConsume 消费流水
FlowTypeConsume FlowType = 2
// FlowTypeRefund 退款流水
FlowTypeRefund FlowType = 3
)
// IntracityDeliveryStatus 配送单状态
type IntracityDeliveryStatus int32
const (
// IntracityDeliveryStatusReady 配送单待接单
IntracityDeliveryStatusReady IntracityDeliveryStatus = 100
// IntracityDeliveryStatusPickedUp 配送单待取货
IntracityDeliveryStatusPickedUp IntracityDeliveryStatus = 101
// IntracityDeliveryStatusOngoing 配送单配送中
IntracityDeliveryStatusOngoing IntracityDeliveryStatus = 102
// IntracityDeliveryStatusFinished 配送单已送达
IntracityDeliveryStatusFinished IntracityDeliveryStatus = 200
// IntracityDeliveryStatusCancelled 配送单已取消
IntracityDeliveryStatusCancelled IntracityDeliveryStatus = 300
// IntracityDeliveryStatusAbnormal 配送单异常
IntracityDeliveryStatusAbnormal IntracityDeliveryStatus = 400
)
// IntracityAddressInfo 门店地址信息
type IntracityAddressInfo struct {
Province string `json:"province"` // 省/自治区/直辖市
City string `json:"city"` // 地级市
Area string `json:"area"` // 县/县级市/区
Street string `json:"street"` // 街道
House string `json:"house"` // 具体门牌号或详细地址
Lat float64 `json:"lat"` // 门店所在地纬度
Lng float64 `json:"lng"` // 门店所在地经度
Phone string `json:"phone"` // 门店联系电话
Name string `json:"name,omitempty"` // 联系人姓名(收货地址时使用)
}
// IntracityStoreInfo 门店信息
type IntracityStoreInfo struct {
WxStoreID string `json:"wx_store_id"` // 微信门店编号
OutStoreID string `json:"out_store_id"` // 自定义门店编号
CityID string `json:"city_id"` // 门店所在城市ID
StoreName string `json:"store_name"` // 门店名称
OrderPattern OrderPattern `json:"order_pattern"` // 运力偏好
ServiceTransPrefer string `json:"service_trans_prefer"` // 优先使用的运力ID
AddressInfo IntracityAddressInfo `json:"address_info"` // 门店地址信息
}
// ============ 门店管理接口 ============
// IntracityApply 开通门店权限
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/intracity_service.html
func (express *Express) IntracityApply(ctx context.Context) error {
accessToken, err := express.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf(intracityApplyURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, map[string]interface{}{})
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "IntracityApply")
}
// CreateStoreRequest 创建门店请求参数
type CreateStoreRequest struct {
OutStoreID string `json:"out_store_id"` // 自定义门店编号
StoreName string `json:"store_name"` // 门店名称
OrderPattern OrderPattern `json:"order_pattern,omitempty"` // 运力偏好:1-价格优先,2-运力优先
ServiceTransPrefer string `json:"service_trans_prefer,omitempty"` // 优先使用的运力ID,order_pattern=2时必填
AddressInfo IntracityAddressInfo `json:"address_info"` // 门店地址信息
}
// CreateStoreResponse 创建门店返回参数
type CreateStoreResponse struct {
util.CommonError
WxStoreID string `json:"wx_store_id"` // 微信门店编号
AppID string `json:"appid"` // 小程序appid
OutStoreID string `json:"out_store_id"` // 自定义门店ID
}
// IntracityCreateStore 创建门店
func (express *Express) IntracityCreateStore(ctx context.Context, req *CreateStoreRequest) (res CreateStoreResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(intracityCreateStoreURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return
}
err = util.DecodeWithError(response, &res, "IntracityCreateStore")
return
}
// QueryStoreRequest 查询门店请求参数
type QueryStoreRequest struct {
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号
OutStoreID string `json:"out_store_id,omitempty"` // 自定义门店编号
}
// QueryStoreResponse 查询门店返回参数
type QueryStoreResponse struct {
util.CommonError
Total uint32 `json:"total"` // 符合条件的门店总数
AppID string `json:"appid"` // 小程序appid
StoreList []IntracityStoreInfo `json:"store_list"` // 门店信息列表
}
// IntracityQueryStore 查询门店
func (express *Express) IntracityQueryStore(ctx context.Context, req *QueryStoreRequest) (res QueryStoreResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(intracityQueryStoreURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return
}
err = util.DecodeWithError(response, &res, "IntracityQueryStore")
return
}
// UpdateStoreKeyInfo 更新门店的key信息
type UpdateStoreKeyInfo struct {
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号
OutStoreID string `json:"out_store_id,omitempty"` // 自定义门店编号,二选一
}
// UpdateStoreContent 更新门店的内容
type UpdateStoreContent struct {
StoreName string `json:"store_name,omitempty"` // 门店名称
OrderPattern OrderPattern `json:"order_pattern,omitempty"` // 运力偏好
ServiceTransPrefer string `json:"service_trans_prefer,omitempty"` // 优先使用的运力ID
AddressInfo *IntracityAddressInfo `json:"address_info,omitempty"` // 门店地址信息
}
// UpdateStoreRequest 更新门店请求参数
type UpdateStoreRequest struct {
Keys UpdateStoreKeyInfo `json:"keys"` // 门店编号
Content UpdateStoreContent `json:"content"` // 更新内容
}
// IntracityUpdateStore 更新门店
func (express *Express) IntracityUpdateStore(ctx context.Context, req *UpdateStoreRequest) error {
accessToken, err := express.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf(intracityUpdateStoreURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "IntracityUpdateStore")
}
// ============ 充值退款接口 ============
// StoreChargeRequest 门店运费充值请求参数
type StoreChargeRequest struct {
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号,pay_mode=PAY_MODE_STORE时必传
ServiceTransID string `json:"service_trans_id"` // 运力ID
Amount uint32 `json:"amount"` // 充值金额,单位:分,50元起充
PayMode PayMode `json:"pay_mode,omitempty"` // 充值主体
}
// StoreChargeResponse 门店运费充值返回参数
type StoreChargeResponse struct {
util.CommonError
PayURL string `json:"payurl"` // 充值页面地址
AppID string `json:"appid"` // 小程序appid
WxStoreID string `json:"wx_store_id"` // 微信门店编号
}
// IntracityStoreCharge 门店运费充值
func (express *Express) IntracityStoreCharge(ctx context.Context, req *StoreChargeRequest) (res StoreChargeResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(intracityStoreChargeURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return
}
err = util.DecodeWithError(response, &res, "IntracityStoreCharge")
return
}
// StoreRefundRequest 门店运费退款请求参数
type StoreRefundRequest struct {
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号
PayMode PayMode `json:"pay_mode,omitempty"` // 充值/扣费主体
ServiceTransID string `json:"service_trans_id"` // 运力ID
}
// StoreRefundResponse 门店运费退款返回参数
type StoreRefundResponse struct {
util.CommonError
AppID string `json:"appid"` // 小程序appid
WxStoreID string `json:"wx_store_id"` // 微信门店编号
RefundAmount uint32 `json:"refund_amount"` // 退款金额,单位:分
}
// IntracityStoreRefund 门店运费退款
func (express *Express) IntracityStoreRefund(ctx context.Context, req *StoreRefundRequest) (res StoreRefundResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(intracityStoreRefundURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return
}
err = util.DecodeWithError(response, &res, "IntracityStoreRefund")
return
}
// QueryFlowRequest 门店运费流水查询请求参数
type QueryFlowRequest struct {
WxStoreID string `json:"wx_store_id"` // 微信门店编号
FlowType FlowType `json:"flow_type"` // 流水类型:1-充值,2-消费,3-退款
ServiceTransID string `json:"service_trans_id,omitempty"` // 运力ID
BeginTime uint32 `json:"begin_time,omitempty"` // 开始时间戳
EndTime uint32 `json:"end_time,omitempty"` // 结束时间戳
PayMode PayMode `json:"pay_mode"` // 扣费主体
}
// FlowRecordInfo 流水记录信息
type FlowRecordInfo struct {
FlowType FlowType `json:"flow_type"` // 流水类型
AppID string `json:"appid"` // appid
WxStoreID string `json:"wx_store_id"` // 微信门店ID
PayOrderID uint64 `json:"pay_order_id,omitempty"` // 充值订单号
WxOrderID string `json:"wx_order_id,omitempty"` // 订单ID(消费流水)
ServiceTransID string `json:"service_trans_id"` // 运力ID
OpenID string `json:"openid,omitempty"` // 用户openid(消费流水)
DeliveryStatus int32 `json:"delivery_status,omitempty"` // 运单状态(消费流水)
PayAmount int32 `json:"pay_amount"` // 支付金额,单位:分
PayTime uint32 `json:"pay_time,omitempty"` // 支付时间
PayStatus string `json:"pay_status,omitempty"` // 支付状态
RefundStatus string `json:"refund_status,omitempty"` // 退款状态
RefundAmount int32 `json:"refund_amount,omitempty"` // 退款金额
RefundTime uint32 `json:"refund_time,omitempty"` // 退款时间
DeductAmount int32 `json:"deduct_amount,omitempty"` // 扣除违约金
CreateTime uint32 `json:"create_time"` // 创建时间
ConsumeDeadline uint32 `json:"consume_deadline,omitempty"` // 有效截止日期
BillID string `json:"bill_id,omitempty"` // 运单ID
DeliveryFinishedTime uint32 `json:"delivery_finished_time,omitempty"` // 运单完成配送的时间
}
// QueryFlowResponse 门店运费流水查询返回参数
type QueryFlowResponse struct {
util.CommonError
Total uint32 `json:"total"` // 总数
FlowList []FlowRecordInfo `json:"flow_list"` // 流水数组
TotalPayAmt int `json:"total_pay_amt"` // 总支付金额
TotalRefundAmt int `json:"total_refund_amt"` // 总退款金额
TotalDeductAmt int `json:"total_deduct_amt"` // 总违约金(消费流水返回)
}
// IntracityQueryFlow 门店运费流水查询
func (express *Express) IntracityQueryFlow(ctx context.Context, req *QueryFlowRequest) (res QueryFlowResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(intracityQueryFlowURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return
}
err = util.DecodeWithError(response, &res, "IntracityQueryFlow")
return
}
// BalanceQueryRequest 门店余额查询请求参数
type BalanceQueryRequest struct {
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号
ServiceTransID string `json:"service_trans_id,omitempty"` // 运力ID
PayMode PayMode `json:"pay_mode,omitempty"` // 充值/扣费主体
}
// BalanceInfo 余额信息
type BalanceInfo struct {
ServiceTransID string `json:"service_trans_id"` // 运力ID
Balance int32 `json:"balance"` // 余额,单位:分
}
// BalanceQueryResponse 门店余额查询返回参数
type BalanceQueryResponse struct {
util.CommonError
AppID string `json:"appid"` // 小程序appid
WxStoreID string `json:"wx_store_id"` // 微信门店编号
BalanceList []BalanceInfo `json:"balance_list"` // 余额列表
}
// IntracityBalanceQuery 门店余额查询
func (express *Express) IntracityBalanceQuery(ctx context.Context, req *BalanceQueryRequest) (res BalanceQueryResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(intracityBalanceQueryURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return
}
err = util.DecodeWithError(response, &res, "IntracityBalanceQuery")
return
}
// ============ 配送订单接口 ============
// CargoInfo 货物信息
type CargoInfo struct {
Name string `json:"name"` // 货物名称
Num uint32 `json:"num,omitempty"` // 货物数量
Price uint32 `json:"price,omitempty"` // 货物价格,单位:分
Weight uint32 `json:"weight,omitempty"` // 货物重量,单位:克
}
// PreAddOrderRequest 预下配送单请求参数
type PreAddOrderRequest struct {
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号,二选一
OutStoreID string `json:"out_store_id,omitempty"` // 自定义门店编号,二选一
UserOpenID string `json:"user_openid"` // 用户openid
UserPhone string `json:"user_phone,omitempty"` // 用户联系电话
UserName string `json:"user_name,omitempty"` // 用户姓名
UserLat float64 `json:"user_lat"` // 用户地址纬度
UserLng float64 `json:"user_lng"` // 用户地址经度
UserAddress string `json:"user_address"` // 用户详细地址
ServiceTransID string `json:"service_trans_id,omitempty"` // 运力ID,不传则查询所有运力
PayMode PayMode `json:"pay_mode,omitempty"` // 充值/扣费主体
CargoInfo *CargoInfo `json:"cargo_info,omitempty"` // 货物信息
}
// TransInfo 运力信息
type TransInfo struct {
ServiceTransID string `json:"service_trans_id"` // 运力ID
ServiceName string `json:"service_name"` // 运力名称
Price uint32 `json:"price"` // 配送费用,单位:分
Distance uint32 `json:"distance"` // 配送距离,单位:米
Errcode int `json:"errcode"` // 错误码,0表示成功
Errmsg string `json:"errmsg"` // 错误信息
}
// PreAddOrderResponse 预下配送单返回参数
type PreAddOrderResponse struct {
util.CommonError
WxStoreID string `json:"wx_store_id"` // 微信门店编号
AppID string `json:"appid"` // 小程序appid
TransList []TransInfo `json:"trans_list"` // 运力列表
}
// IntracityPreAddOrder 预下配送单(查询运费)
func (express *Express) IntracityPreAddOrder(ctx context.Context, req *PreAddOrderRequest) (res PreAddOrderResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(intracityPreAddOrderURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return
}
err = util.DecodeWithError(response, &res, "IntracityPreAddOrder")
return
}
// AddOrderRequest 创建配送单请求参数
type AddOrderRequest struct {
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号,二选一
OutStoreID string `json:"out_store_id,omitempty"` // 自定义门店编号,二选一
OutOrderID string `json:"out_order_id"` // 自定义订单号,需唯一
ServiceTransID string `json:"service_trans_id,omitempty"` // 运力ID
UserOpenID string `json:"user_openid"` // 用户openid
UserPhone string `json:"user_phone"` // 用户联系电话
UserName string `json:"user_name"` // 用户姓名
UserLat float64 `json:"user_lat"` // 用户地址纬度
UserLng float64 `json:"user_lng"` // 用户地址经度
UserAddress string `json:"user_address"` // 用户详细地址
PayMode PayMode `json:"pay_mode,omitempty"` // 充值/扣费主体
CargoInfo *CargoInfo `json:"cargo_info,omitempty"` // 货物信息
OrderDetailPath string `json:"order_detail_path,omitempty"` // 订单详情页路径
CallbackURL string `json:"callback_url,omitempty"` // 配送状态回调URL
UseInsurance uint32 `json:"use_insurance,omitempty"` // 是否使用保价:0-不使用,1-使用
InsuranceValue uint32 `json:"insurance_value,omitempty"` // 保价金额,单位:分
ExpectTime uint32 `json:"expect_time,omitempty"` // 期望送达时间戳
Remark string `json:"remark,omitempty"` // 备注
}
// AddOrderResponse 创建配送单返回参数
type AddOrderResponse struct {
util.CommonError
WxOrderID string `json:"wx_order_id"` // 微信订单号
AppID string `json:"appid"` // 小程序appid
WxStoreID string `json:"wx_store_id"` // 微信门店编号
OutOrderID string `json:"out_order_id"` // 自定义订单号
ServiceTransID string `json:"service_trans_id"` // 运力ID
BillID string `json:"bill_id"` // 运力订单号
Price uint32 `json:"price"` // 配送费用,单位:分
Distance uint32 `json:"distance"` // 配送距离,单位:米
}
// IntracityAddOrder 创建配送单
func (express *Express) IntracityAddOrder(ctx context.Context, req *AddOrderRequest) (res AddOrderResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(intracityAddOrderURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return
}
err = util.DecodeWithError(response, &res, "IntracityAddOrder")
return
}
// QueryOrderRequest 查询配送单请求参数
type QueryOrderRequest struct {
WxOrderID string `json:"wx_order_id,omitempty"` // 微信订单号,二选一
OutOrderID string `json:"out_order_id,omitempty"` // 自定义订单号,二选一
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号
OutStoreID string `json:"out_store_id,omitempty"` // 自定义门店编号
}
// RiderInfo 骑手信息
type RiderInfo struct {
Name string `json:"name"` // 骑手姓名
Phone string `json:"phone"` // 骑手电话
RiderCode string `json:"rider_code"` // 骑手编号
RiderImgURL string `json:"rider_img_url"` // 骑手头像URL
}
// QueryOrderResponse 查询配送单返回参数
type QueryOrderResponse struct {
util.CommonError
WxOrderID string `json:"wx_order_id"` // 微信订单号
AppID string `json:"appid"` // 小程序appid
WxStoreID string `json:"wx_store_id"` // 微信门店编号
OutOrderID string `json:"out_order_id"` // 自定义订单号
ServiceTransID string `json:"service_trans_id"` // 运力ID
BillID string `json:"bill_id"` // 运力订单号
DeliveryStatus IntracityDeliveryStatus `json:"delivery_status"` // 配送状态
Price uint32 `json:"price"` // 配送费用,单位:分
Distance uint32 `json:"distance"` // 配送距离,单位:米
CreateTime uint32 `json:"create_time"` // 订单创建时间
RiderInfo *RiderInfo `json:"rider_info"` // 骑手信息
FinishTime uint32 `json:"finish_time"` // 订单完成时间
}
// IntracityQueryOrder 查询配送单
func (express *Express) IntracityQueryOrder(ctx context.Context, req *QueryOrderRequest) (res QueryOrderResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(intracityQueryOrderURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return
}
err = util.DecodeWithError(response, &res, "IntracityQueryOrder")
return
}
// CancelOrderRequest 取消配送单请求参数
type CancelOrderRequest struct {
WxOrderID string `json:"wx_order_id,omitempty"` // 微信订单号,二选一
OutOrderID string `json:"out_order_id,omitempty"` // 自定义订单号,二选一
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号
OutStoreID string `json:"out_store_id,omitempty"` // 自定义门店编号
CancelReason string `json:"cancel_reason,omitempty"` // 取消原因
}
// CancelOrderResponse 取消配送单返回参数
type CancelOrderResponse struct {
util.CommonError
WxOrderID string `json:"wx_order_id"` // 微信订单号
RefundAmount int32 `json:"refund_amount"` // 退款金额,单位:分
DeductAmount int32 `json:"deduct_amount"` // 扣除违约金,单位:分
}
// IntracityCancelOrder 取消配送单
func (express *Express) IntracityCancelOrder(ctx context.Context, req *CancelOrderRequest) (res CancelOrderResponse, err error) {
accessToken, err := express.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(intracityCancelOrderURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return
}
err = util.DecodeWithError(response, &res, "IntracityCancelOrder")
return
}
// MockNotifyRequest 模拟配送回调请求参数(仅用于测试)
type MockNotifyRequest struct {
WxOrderID string `json:"wx_order_id"` // 微信订单号
DeliveryStatus IntracityDeliveryStatus `json:"delivery_status"` // 配送状态
}
// IntracityMockNotify 模拟配送回调(仅用于测试)
func (express *Express) IntracityMockNotify(ctx context.Context, req *MockNotifyRequest) error {
accessToken, err := express.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf(intracityMockNotifyURL, accessToken)
response, err := util.PostJSONContext(ctx, uri, req)
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "IntracityMockNotify")
}
+84 -1
View File
@@ -41,6 +41,12 @@ const (
EventTypeXpayGoodsDeliverNotify EventType = "xpay_goods_deliver_notify"
// EventTypeXpayCoinPayNotify 代币支付推送事件
EventTypeXpayCoinPayNotify EventType = "xpay_coin_pay_notify"
// EventTypeXpayRefundNotify 退款推送事件
EventTypeXpayRefundNotify EventType = "xpay_refund_notify"
// EventTypeXpaySubscribeIosRefundQueryNotify iOS Apple 支付退款问询事件
EventTypeXpaySubscribeIosRefundQueryNotify EventType = "xpay_subscribe_ios_refund_query_notify"
// EventTypeXpayComplaintNotify 用户投诉推送事件
EventTypeXpayComplaintNotify EventType = "xpay_complaint_notify"
// EventSubscribePopup 用户操作订阅通知弹窗事件推送,用户在图文等场景内订阅通知的操作
EventSubscribePopup EventType = "subscribe_msg_popup_event"
// EventSubscribeMsgChange 用户管理订阅通知,用户在服务通知管理页面做通知管理时的操作
@@ -203,6 +209,14 @@ func (receiver *PushReceiver) getEvent(dataType string, eventType EventType, dec
var pushData PushDataXpayCoinPayNotify
err := receiver.unmarshal(dataType, decryptMsg, &pushData)
return &pushData, err
case EventTypeXpayRefundNotify:
// 退款推送事件
return receiver.unmarshalPushData(dataType, decryptMsg, &PushDataXpayRefundNotify{})
case EventTypeXpaySubscribeIosRefundQueryNotify: // iOS Apple 支付退款问询事件
return receiver.unmarshalPushData(dataType, decryptMsg, &PushDataXpaySubscribeIosRefundQueryNotify{})
case EventTypeXpayComplaintNotify:
// 用户投诉推送事件
return receiver.unmarshalPushData(dataType, decryptMsg, &PushDataXpayComplaintNotify{})
case EventSubscribePopup:
// 用户操作订阅通知弹窗事件推送
return receiver.unmarshalSubscribePopup(dataType, decryptMsg)
@@ -225,6 +239,12 @@ func (receiver *PushReceiver) unmarshal(dataType string, decryptMsg []byte, push
return json.Unmarshal(decryptMsg, pushData)
}
// unmarshalPushData unmarshal push data helper
func (receiver *PushReceiver) unmarshalPushData(dataType string, decryptMsg []byte, pushData interface{}) (PushData, error) {
err := receiver.unmarshal(dataType, decryptMsg, pushData)
return pushData, err
}
// unmarshalSubscribePopup
func (receiver *PushReceiver) unmarshalSubscribePopup(dataType string, decryptMsg []byte) (PushData, error) {
var pushData PushDataSubscribePopup
@@ -432,6 +452,7 @@ type PushDataXpayGoodsDeliverNotify struct {
Env int `json:"Env" xml:"Env"` // ,环境配置 0:现网环境(也叫正式环境)1:沙箱环境
WeChatPayInfo WeChatPayInfo `json:"WeChatPayInfo" xml:"WeChatPayInfo"` // 微信支付信息 非微信支付渠道可能没有
GoodsInfo GoodsInfo `json:"GoodsInfo" xml:"GoodsInfo"` // 道具参数信息
TeamInfo XpayTeamInfo `json:"TeamInfo" xml:"TeamInfo"` // 拼团信息
}
// WeChatPayInfo 微信支付信息
@@ -556,7 +577,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"`
}
@@ -577,3 +598,65 @@ func (s *PushDataSubscribeMsgSent) GetSubscribeMsgSentEvents() []SubscribeMsgSen
return s.SubscribeMsgSentEvent.List
}
// XpayTeamInfo team purchase info
type XpayTeamInfo struct {
ActivityID string `json:"ActivityId" xml:"ActivityId"` // 活动 id
TeamID string `json:"TeamId" xml:"TeamId"` // 团 id
TeamType int `json:"TeamType" xml:"TeamType"` // 团类型 1-支付全部,拼成退款
TeamAction int `json:"TeamAction" xml:"TeamAction"` // 0-创团 1-参团
}
// PushDataXpayRefundNotify 退款推送
type PushDataXpayRefundNotify struct {
CommonPushData
OpenID string `json:"OpenId" xml:"OpenId"` // 用户 openid
WxRefundID string `json:"WxRefundId" xml:"WxRefundId"` // 微信退款单号
MchRefundID string `json:"MchRefundId" xml:"MchRefundId"` // 商户退款单号
WxOrderID string `json:"WxOrderId" xml:"WxOrderId"` // 退款单对应支付单的微信单号
MchOrderID string `json:"MchOrderId" xml:"MchOrderId"` // 退款单对应支付单的商户单号
RefundFee int `json:"RefundFee" xml:"RefundFee"` // 退款金额,单位分
RetCode int `json:"RetCode" xml:"RetCode"` // 退款结果,0 为成功
RetMsg string `json:"RetMsg" xml:"RetMsg"` // 退款结果详情
RefundStartTimestamp int64 `json:"RefundStartTimestamp" xml:"RefundStartTimestamp"` // 开始退款时间,秒级时间戳
RefundSuccTimestamp int64 `json:"RefundSuccTimestamp" xml:"RefundSuccTimestamp"` // 结束退款时间,秒级时间戳
WxpayRefundTransactionID string `json:"WxpayRefundTransactionId" xml:"WxpayRefundTransactionId"` // 退款单的微信支付单号
RetryTimes int `json:"RetryTimes" xml:"RetryTimes"` // 重试次数,从 0 开始
TeamInfo XpayTeamInfo `json:"TeamInfo" xml:"TeamInfo"` // 拼团信息
}
// PushDataXpaySubscribeIosRefundQueryNotify iOS Apple 支付退款问询事件
// 文档:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment/ios.html
type PushDataXpaySubscribeIosRefundQueryNotify struct {
CommonPushData
RefundTime string `json:"refund_time" xml:"refund_time"` // 问询时间,Unix 时间戳
OrderTime string `json:"order_time" xml:"order_time"` // 该笔退款的订单时间,Unix 时间戳
ChannelBill string `json:"channel_bill" xml:"channel_bill"` // Apple 支付票据号
BundleID string `json:"bundleid" xml:"bundleid"` // 应用的 Apple bundleid
ProductID string `json:"product_id" xml:"product_id"` // 道具 id
PCount string `json:"p_count" xml:"p_count"` // 道具/代币数量
RefundRequestReason string `json:"refund_request_reason" xml:"refund_request_reason"` // 用户请求退款的原因
ProvideStatus string `json:"provide_status" xml:"provide_status"` // 发货状态,0:未发货 1:已发货 2:发货中
PayOrderID string `json:"pay_order_id" xml:"pay_order_id"` // 退款对应支付订单号
}
// IosRefundQueryResponse iOS Apple 支付退款问询应答
type IosRefundQueryResponse struct {
ResultCode int32 `json:"result_code" xml:"result_code"` // 结果码,0-放过,建议退款;1-拦截,拒绝退款
ResultInfo string `json:"result_info" xml:"result_info"` // 结果描述
Evidence string `json:"evidence" xml:"evidence"` // 决策凭据,必填,用于退款审计
}
// PushDataXpayComplaintNotify 用户投诉推送
type PushDataXpayComplaintNotify struct {
CommonPushData
OpenID string `json:"OpenId" xml:"OpenId"` // 用户 openid
WxOrderID string `json:"WxOrderId" xml:"WxOrderId"` // 微信单号
MchOrderID string `json:"MchOrderId" xml:"MchOrderId"` // 商户单号
TransactionID string `json:"TransactionId" xml:"TransactionId"` // 微信支付交易单号
ComplaintID string `json:"ComplaintId" xml:"ComplaintId"` // 投诉单号
ComplaintDetail string `json:"ComplaintDetail" xml:"ComplaintDetail"` // 投诉详情
ComplaintTime int64 `json:"ComplaintTime" xml:"ComplaintTime"` // 投诉时间,秒级时间戳
RetryTimes int `json:"RetryTimes" xml:"RetryTimes"` // 重试次数,从 0 开始
RequestID string `json:"RequestId" xml:"RequestId"` // 请求编号
}
+51 -6
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")
}
+23
View File
@@ -10,8 +10,11 @@ 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/ocr"
"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 +182,23 @@ 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)
}
// GetOCR OCR接口
func (miniProgram *MiniProgram) GetOCR() *ocr.OCR {
return ocr.NewOCR(miniProgram.ctx)
}
// GetIntracity 同城配送接口
func (miniProgram *MiniProgram) GetIntracity() *express.Express {
return express.NewExpress(miniProgram.ctx)
}
+248
View File
@@ -0,0 +1,248 @@
package ocr
import (
"fmt"
"net/url"
"github.com/silenceper/wechat/v2/miniprogram/context"
"github.com/silenceper/wechat/v2/util"
)
const (
ocrIDCardURL = "https://api.weixin.qq.com/cv/ocr/idcard"
ocrBankCardURL = "https://api.weixin.qq.com/cv/ocr/bankcard"
ocrDrivingURL = "https://api.weixin.qq.com/cv/ocr/driving"
ocrDrivingLicenseURL = "https://api.weixin.qq.com/cv/ocr/drivinglicense"
ocrBizLicenseURL = "https://api.weixin.qq.com/cv/ocr/bizlicense"
ocrCommonURL = "https://api.weixin.qq.com/cv/ocr/comm"
)
// OCR struct
type OCR struct {
*context.Context
}
// coordinate 坐标
type coordinate struct {
X int64 `json:"x,omitempty"`
Y int64 `json:"y,omitempty"`
}
// position 位置
type position struct {
LeftTop coordinate `json:"left_top"`
RightTop coordinate `json:"right_top"`
RightBottom coordinate `json:"right_bottom"`
LeftBottom coordinate `json:"left_bottom"`
}
// imageSize 图片尺寸
type imageSize struct {
Width int64 `json:"w,omitempty"`
Height int64 `json:"h,omitempty"`
}
// ResDriving 行驶证返回结果
type ResDriving struct {
util.CommonError
PlateNumber string `json:"plate_num,omitempty"`
VehicleType string `json:"vehicle_type,omitempty"`
Owner string `json:"owner,omitempty"`
Address string `json:"addr,omitempty"`
UseCharacter string `json:"use_character,omitempty"`
Model string `json:"model,omitempty"`
Vin string `json:"vin,omitempty"`
EngineNumber string `json:"engine_num,omitempty"`
RegisterDate string `json:"register_date,omitempty"`
IssueDate string `json:"issue_date,omitempty"`
PlateNumberB string `json:"plate_num_b,omitempty"`
Record string `json:"record,omitempty"`
PassengersNumber string `json:"passengers_num,omitempty"`
TotalQuality string `json:"total_quality,omitempty"`
PrepareQuality string `json:"prepare_quality,omitempty"`
OverallSize string `json:"overall_size,omitempty"`
CardPositionFront map[string]position `json:"card_position_front,omitempty"`
CardPositionBack map[string]position `json:"card_position_back,omitempty"`
ImageSize imageSize `json:"img_size,omitempty"`
}
// ResIDCard 身份证返回结果
type ResIDCard struct {
util.CommonError
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
ID string `json:"id,omitempty"`
Address string `json:"addr,omitempty"`
Gender string `json:"gender,omitempty"`
Nationality string `json:"nationality,omitempty"`
ValidDate string `json:"valid_date,omitempty"`
}
// ResBankCard 银行卡返回结果
type ResBankCard struct {
util.CommonError
Number string `json:"number,omitempty"`
}
// ResDrivingLicense 驾驶证返回结果
type ResDrivingLicense struct {
util.CommonError
IDNumber string `json:"id_num,omitempty"`
Name string `json:"name,omitempty"`
Sex string `json:"sex,omitempty"`
Nationality string `json:"nationality,omitempty"`
Address string `json:"address,omitempty"`
Birthday string `json:"birth_date,omitempty"`
IssueDate string `json:"issue_date,omitempty"`
CarClass string `json:"car_class,omitempty"`
ValidFrom string `json:"valid_from,omitempty"`
ValidTo string `json:"valid_to,omitempty"`
OfficialSeal string `json:"official_seal,omitempty"`
}
// ResBizLicense 营业执照返回结果
type ResBizLicense struct {
util.CommonError
RegisterNumber string `json:"reg_num,omitempty"`
Serial string `json:"serial,omitempty"`
LegalRepresentative string `json:"legal_representative,omitempty"`
EnterpriseName string `json:"enterprise_name,omitempty"`
TypeOfOrganization string `json:"type_of_organization,omitempty"`
Address string `json:"address,omitempty"`
TypeOfEnterprise string `json:"type_of_enterprise,omitempty"`
BusinessScope string `json:"business_scope,omitempty"`
RegisteredCapital string `json:"registered_capital,omitempty"`
PaidInCapital string `json:"paid_in_capital,omitempty"`
ValidPeriod string `json:"valid_period,omitempty"`
RegisterDate string `json:"registered_date,omitempty"`
CertPosition map[string]position `json:"cert_position,omitempty"`
ImageSize imageSize `json:"img_size,omitempty"`
}
// ResCommon 公共印刷品返回结果
type ResCommon struct {
util.CommonError
Items []commonItem `json:"items,omitempty"`
ImageSize imageSize `json:"img_size,omitempty"`
}
// commonItem 公共元素
type commonItem struct {
Position position `json:"pos"`
Text string `json:"text"`
}
// NewOCR 实例
func NewOCR(c *context.Context) *OCR {
ocr := new(OCR)
ocr.Context = c
return ocr
}
// IDCard 身份证OCR识别接口
func (ocr *OCR) IDCard(path string) (resIDCard ResIDCard, err error) {
accessToken, err := ocr.GetAccessToken()
if err != nil {
return
}
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrIDCardURL, url.QueryEscape(path), accessToken), "")
if err != nil {
return
}
err = util.DecodeWithError(response, &resIDCard, "OCRIDCard")
return
}
// BankCard 银行卡OCR识别接口
func (ocr *OCR) BankCard(path string) (resBankCard ResBankCard, err error) {
accessToken, err := ocr.GetAccessToken()
if err != nil {
return
}
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrBankCardURL, url.QueryEscape(path), accessToken), "")
if err != nil {
return
}
err = util.DecodeWithError(response, &resBankCard, "OCRBankCard")
return
}
// Driving 行驶证OCR识别接口
func (ocr *OCR) Driving(path string) (resDriving ResDriving, err error) {
accessToken, err := ocr.GetAccessToken()
if err != nil {
return
}
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrDrivingURL, url.QueryEscape(path), accessToken), "")
if err != nil {
return
}
err = util.DecodeWithError(response, &resDriving, "OCRDriving")
return
}
// DrivingLicense 驾驶证OCR识别接口
func (ocr *OCR) DrivingLicense(path string) (resDrivingLicense ResDrivingLicense, err error) {
accessToken, err := ocr.GetAccessToken()
if err != nil {
return
}
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrDrivingLicenseURL, url.QueryEscape(path), accessToken), "")
if err != nil {
return
}
err = util.DecodeWithError(response, &resDrivingLicense, "OCRDrivingLicense")
return
}
// BizLicense 营业执照OCR识别接口
func (ocr *OCR) BizLicense(path string) (resBizLicense ResBizLicense, err error) {
accessToken, err := ocr.GetAccessToken()
if err != nil {
return
}
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrBizLicenseURL, url.QueryEscape(path), accessToken), "")
if err != nil {
return
}
err = util.DecodeWithError(response, &resBizLicense, "OCRBizLicense")
return
}
// Common 通用印刷体OCR识别接口
func (ocr *OCR) Common(path string) (resCommon ResCommon, err error) {
accessToken, err := ocr.GetAccessToken()
if err != nil {
return
}
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrCommonURL, url.QueryEscape(path), accessToken), "")
if err != nil {
return
}
err = util.DecodeWithError(response, &resCommon, "OCRCommon")
return
}
+456
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
}
+1 -1
View File
@@ -55,7 +55,7 @@ type QRCoder struct {
// envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop"
EnvVersion string `json:"env_version,omitempty"`
// ShowSplashAd 控制通过该小程序码进入小程序是否展示封面广告1、默认为true,展示封面广告2、传入为false时,不展示封面广告
ShowSplashAd bool `json:"show_splash_ad,omitempty"`
ShowSplashAd *bool `json:"show_splash_ad,omitempty"`
}
// fetchCode 请求并返回二维码二进制数据
+15 -4
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
}
+221 -9
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
}
+29 -13
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
}
return u.QueryWithType(&ULQueryRequest{URLLink: urlLink})
}
uri := fmt.Sprintf("%s?access_token=%s", queryURL, accessToken)
response, err := util.PostJSON(uri, map[string]string{"url_link": urlLink})
if err != nil {
// 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 resp ULQueryResult
err = util.DecodeWithError(response, &resp, "URLLink.Query")
if err != nil {
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(queryURL, accessToken), req); err != nil {
return nil, err
}
return &resp, nil
result := &ULQueryResult{}
err = util.DecodeWithError(response, result, "URLLink.Query")
return result, err
}
+31 -17
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
}
+31 -5
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
}
+75
View File
@@ -51,6 +51,30 @@ const (
SessionKeyExpiredError ErrCode = 268490009
// BillGeneratingError 错误码 268490011、账单数据生成中,请稍后调用本接口获取
BillGeneratingError ErrCode = 268490011
// BatchTaskRunningError 错误码 268490012、批量任务运行中,请等待完成后才能再次运行
BatchTaskRunningError ErrCode = 268490012
// RefundVerifiedOrderError 错误码 268490013、禁止对核销状态的单进行退款
RefundVerifiedOrderError ErrCode = 268490013
// RefundInProgressError 错误码 268490014、退款操作进行中,稍后可以使用相同参数重试
RefundInProgressError ErrCode = 268490014
// FrequencyLimitError 错误码 268490015、频率限制
FrequencyLimitError ErrCode = 268490015
// RefundLeftFeeMismatchError 错误码 268490016、退款的 left_fee 字段与实际不符
RefundLeftFeeMismatchError ErrCode = 268490016
// AdFundIndustryMismatchError 错误码 268490018、广告金充值帐户行业 id 不匹配
AdFundIndustryMismatchError ErrCode = 268490018
// AdFundAccountBoundError 错误码 268490019、广告金充值帐户 id 已绑定其他 appid
AdFundAccountBoundError ErrCode = 268490019
// AdFundAccountNameError 错误码 268490020、广告金充值帐户主体名称错误
AdFundAccountNameError ErrCode = 268490020
// AccountNotVerifiedError 错误码 268490021、账户未完成进件
AccountNotVerifiedError ErrCode = 268490021
// AdFundAccountInvalidError 错误码 268490022、广告金充值账户无效
AdFundAccountInvalidError ErrCode = 268490022
// AdFundInsufficientError 错误码 268490023、广告金余额不足
AdFundInsufficientError ErrCode = 268490023
// AdFundAmountMustPositiveError 错误码 268490024、广告金充值金额必须大于 0
AdFundAmountMustPositiveError ErrCode = 268490024
)
const (
@@ -117,6 +141,57 @@ const (
// queryPublishGoods 查询批量发布道具任务状态
queryPublishGoods = "/xpay/query_publish_goods"
// startDownloadOrder 发起下载小程序订单明细任务
startDownloadOrder = "/xpay/start_download_order"
// queryDownloadOrder 查询下载订单任务结果
queryDownloadOrder = "/xpay/query_download_order"
// queryBizBalance 查询商家账户可提现余额
queryBizBalance = "/xpay/query_biz_balance"
// queryTransferAccount 查询广告金充值账户
queryTransferAccount = "/xpay/query_transfer_account"
// queryAdverFunds 查询广告金发放记录
queryAdverFunds = "/xpay/query_adver_funds"
// createFundsBill 充值广告金
createFundsBill = "/xpay/create_funds_bill"
// bindTransferAccount 绑定广告金充值账户
bindTransferAccount = "/xpay/bind_transfer_accout"
// queryFundsBill 查询广告金充值记录
queryFundsBill = "/xpay/query_funds_bill"
// queryRecoverBill 查询广告金回收记录
queryRecoverBill = "/xpay/query_recover_bill"
// downloadAdverFundsOrder 下载广告金对应的商户订单信息
downloadAdverFundsOrder = "/xpay/download_adverfunds_order"
// getComplaintList 获取投诉列表
getComplaintList = "/xpay/get_complaint_list"
// getComplaintDetail 获取投诉详情
getComplaintDetail = "/xpay/get_complaint_detail"
// getNegotiationHistory 获取协商历史
getNegotiationHistory = "/xpay/get_negotiation_history"
// responseComplaint 回复用户
responseComplaint = "/xpay/response_complaint"
// completeComplaint 完成投诉处理
completeComplaint = "/xpay/complete_complaint"
// uploadVPFile 上传媒体文件
uploadVPFile = "/xpay/upload_vp_file"
// getUploadFileSign 获取微信支付反馈投诉图片的签名头部
getUploadFileSign = "/xpay/get_upload_file_sign"
)
const (
+443 -1
View File
@@ -25,7 +25,7 @@ import (
)
// VirtualPayment mini program virtual payment
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/virtual-payment.html#_2-3-%E6%9C%8D%E5%8A%A1%E5%99%A8API
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.html
type VirtualPayment struct {
ctx *context.Context
sessionKey string
@@ -368,12 +368,14 @@ type AsyncXPayGoodsDeliverNotifyRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
WechatPayInfo *WeChatPayInfo `json:"WechatPayInfo"` // 微信支付订单信息
GoodsInfo *GoodsInfo `json:"GoodsInfo"` // 道具信息
TeamInfo *TeamInfo `json:"TeamInfo"` // 拼团信息
}
// WeChatPayInfo 微信支付信息 非微信支付渠道可能没有
type WeChatPayInfo struct {
MchOrderNo string `json:"MchOrderNo"` // 商户订单号
TransactionID string `json:"TransactionId"` // 微信支付订单号
PaidTime int64 `json:"PaidTime"` // 用户支付时间,Linux 秒级时间戳
}
// GoodsInfo 道具参数信息
@@ -385,6 +387,14 @@ type GoodsInfo struct {
Attach string `json:"Attach"` // 透传信息
}
// TeamInfo 拼团信息
type TeamInfo struct {
ActivityID string `json:"ActivityId"` // 活动 id
TeamID string `json:"TeamId"` // 团 id
TeamType int `json:"TeamType"` // 团类型 1-支付全部,拼成退款
TeamAction int `json:"TeamAction"` // 0-创团 1-参团
}
// AsyncXPayGoodsDeliverNotifyResponse 异步通知发货响应参数
type AsyncXPayGoodsDeliverNotifyResponse struct {
util.CommonError
@@ -417,6 +427,438 @@ type AsyncXPayCoinPayNotifyResponse struct {
util.CommonError
}
// AsyncXPayRefundNotifyRequest 异步通知退款推送,请求参数
type AsyncXPayRefundNotifyRequest struct {
ToUserName string `json:"ToUserName"` // 小程序的原始 ID
FromUserName string `json:"FromUserName"` // 发送方帐号(一个 OpenID)
CreateTime int `json:"CreateTime"` // 消息发送时间(整型)
MsgType string `json:"MsgType"` // 消息类型,此时固定为:event
Event string `json:"Event"` // 事件类型,此时固定为:xpay_refund_notify
Openid string `json:"openid"` // 用户 openid
WxRefundID string `json:"WxRefundId"` // 微信退款单号
MchRefundID string `json:"MchRefundId"` // 商户退款单号
WxOrderID string `json:"WxOrderId"` // 退款单对应支付单的微信单号
MchOrderID string `json:"MchOrderId"` // 退款单对应支付单的商户单号
RefundFee int `json:"RefundFee"` // 退款金额,单位分
RetCode int `json:"RetCode"` // 退款结果,0 为成功,非0 为失败
RetMsg string `json:"RetMsg"` // 退款结果详情
RefundStartTimestamp int64 `json:"RefundStartTimestamp"` // 开始退款时间,秒级时间戳
RefundSuccTimestamp int64 `json:"RefundSuccTimestamp"` // 结束退款时间,秒级时间戳
WxpayRefundTransactionID string `json:"WxpayRefundTransactionId"` // 退款单的微信支付单号
RetryTimes int `json:"RetryTimes"` // 重试次数,从 0 开始
TeamInfo *TeamInfo `json:"TeamInfo"` // 拼团信息
}
// AsyncXPayRefundNotifyResponse 异步通知退款推送响应参数
type AsyncXPayRefundNotifyResponse struct {
util.CommonError
}
// AsyncXPaySubscribeIosRefundQueryNotifyRequest iOS Apple 支付退款问询消息
// 文档:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment/ios.html
type AsyncXPaySubscribeIosRefundQueryNotifyRequest struct {
ToUserName string `json:"ToUserName"` // 小程序的原始 ID
FromUserName string `json:"FromUserName"` // 发送方帐号
CreateTime int `json:"CreateTime"` // 消息发送时间
MsgType string `json:"MsgType"` // 消息类型,此时固定为 event
Event string `json:"Event"` // 事件类型,此时固定为 xpay_subscribe_ios_refund_query_notify
RefundTime string `json:"refund_time"` // 问询时间,Unix 时间戳
OrderTime string `json:"order_time"` // 该笔退款的订单时间,Unix 时间戳
ChannelBill string `json:"channel_bill"` // Apple 支付票据号
BundleID string `json:"bundleid"` // 应用的 Apple bundleid
ProductID string `json:"product_id"` // 道具 id
PCount string `json:"p_count"` // 道具/代币数量
RefundRequestReason string `json:"refund_request_reason"` // 用户请求退款的原因
ProvideStatus string `json:"provide_status"` // 发货状态,0:未发货 1:已发货 2:发货中
PayOrderID string `json:"pay_order_id"` // 退款对应支付订单号
}
// AsyncXPaySubscribeIosRefundQueryNotifyResponse iOS Apple 支付退款问询应答
type AsyncXPaySubscribeIosRefundQueryNotifyResponse struct {
ResultCode int32 `json:"result_code"` // 结果码,0-放过,建议退款;1-拦截,拒绝退款
ResultInfo string `json:"result_info"` // 结果描述
Evidence string `json:"evidence"` // 决策凭据,必填,用于退款审计
}
// AsyncXPayComplaintNotifyRequest 异步通知用户投诉推送,请求参数
type AsyncXPayComplaintNotifyRequest struct {
ToUserName string `json:"ToUserName"` // 小程序的原始 ID
FromUserName string `json:"FromUserName"` // 发送方帐号(一个 OpenID)
CreateTime int `json:"CreateTime"` // 消息发送时间(整型)
MsgType string `json:"MsgType"` // 消息类型,此时固定为:event
Event string `json:"Event"` // 事件类型,此时固定为:xpay_complaint_notify
Openid string `json:"openid"` // 用户 openid
WxOrderID string `json:"WxOrderId"` // 微信单号
MchOrderID string `json:"MchOrderId"` // 商户单号
TransactionID string `json:"TransactionId"` // 微信支付交易单号
ComplaintID string `json:"ComplaintId"` // 投诉单号
ComplaintDetail string `json:"ComplaintDetail"` // 投诉详情
ComplaintTime int64 `json:"ComplaintTime"` // 投诉时间,秒级时间戳
RetryTimes int `json:"RetryTimes"` // 重试次数,从 0 开始
RequestID string `json:"RequestId"` // 请求编号
}
// AsyncXPayComplaintNotifyResponse 异步通知用户投诉推送响应参数
type AsyncXPayComplaintNotifyResponse struct {
util.CommonError
}
// ==================== 下载订单 ====================
// StartDownloadOrderRequest 发起下载小程序订单明细任务,请求参数
type StartDownloadOrderRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
BeginDs string `json:"begin_ds"` // 账单开始日期,格式为 yyyymmdd
EndDs string `json:"end_ds"` // 账单结束日期,格式为 yyyymmdd
}
// StartDownloadOrderResponse 发起下载小程序订单明细任务 响应参数
type StartDownloadOrderResponse struct {
util.CommonError
}
// QueryDownloadOrderRequest 查询下载订单任务结果,请求参数
type QueryDownloadOrderRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
BeginDs string `json:"begin_ds"` // 账单开始日期,格式为 yyyymmdd
EndDs string `json:"end_ds"` // 账单结束日期,格式为 yyyymmdd
}
// QueryDownloadOrderResponse 查询下载订单任务结果 响应参数
type QueryDownloadOrderResponse struct {
util.CommonError
URL string `json:"url"` // 订单下载地址
}
// ==================== 资金管理 ====================
// QueryBizBalanceRequest 查询商家账户可提现余额,请求参数
type QueryBizBalanceRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境(仅作为签名校验,查询结果都是正式环境的)
}
// BizBalanceAvailable 可提现余额
type BizBalanceAvailable struct {
Amount string `json:"amount"` // 可提现余额,单位元
CurrencyCode string `json:"currency_code"` // 币种(一般为 CNY)
}
// QueryBizBalanceResponse 查询商家账户可提现余额 响应参数
type QueryBizBalanceResponse struct {
util.CommonError
BalanceAvailable *BizBalanceAvailable `json:"balance_available"` // 可提现余额
}
// ==================== 广告金 ====================
// TransferAccount 广告金充值账户信息
type TransferAccount struct {
TransferAccountName string `json:"transfer_account_name"` // 账户名称
TransferAccountUID int64 `json:"transfer_account_uid"` // 账户 UID
TransferAccountAgencyID int64 `json:"transfer_account_agency_id"` // 代理商 ID
TransferAccountAgencyName string `json:"transfer_account_agency_name"` // 代理商名称
State int `json:"state"` // 审核状态 0-待审核 1-已通过 2-已驳回
BindResult int `json:"bind_result"` // 绑定结果 1-绑定成功 2-绑定失败
ErrorMsg string `json:"error_msg"` // 错误信息
}
// QueryTransferAccountRequest 查询广告金充值账户,请求参数
type QueryTransferAccountRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
}
// QueryTransferAccountResponse 查询广告金充值账户 响应参数
type QueryTransferAccountResponse struct {
util.CommonError
AcctList []*TransferAccount `json:"acct_list"` // 充值账户列表
}
// AdverFundsFilter 广告金发放记录查询过滤条件
type AdverFundsFilter struct {
SettleBegin int64 `json:"settle_begin,omitempty"` // 结算周期开始时间,unix 秒级时间戳
SettleEnd int64 `json:"settle_end,omitempty"` // 结算周期结束时间,unix 秒级时间戳
FundType int `json:"fund_type,omitempty"` // 资金类型 0-普通赠送 1-广告激励 2-定向激励
}
// AdverFundsRecord 广告金发放记录
type AdverFundsRecord struct {
SettleBegin int64 `json:"settle_begin"` // 结算周期开始时间,unix 秒级时间戳
SettleEnd int64 `json:"settle_end"` // 结算周期结束时间,unix 秒级时间戳
TotalAmount int64 `json:"total_amount"` // 发放总金额,单位分
RemainAmount int64 `json:"remain_amount"` // 剩余可用金额,单位分
ExpireTime int64 `json:"expire_time"` // 过期时间,unix 秒级时间戳
FundType int `json:"fund_type"` // 资金类型 0-普通赠送 1-广告激励 2-定向激励
FundID string `json:"fund_id"` // 广告金发放 ID
}
// QueryAdverFundsRequest 查询广告金发放记录,请求参数
type QueryAdverFundsRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
Page int `json:"page,omitempty"` // 页码,>= 1
PageSize int `json:"page_size,omitempty"` // 每页记录数
Filter *AdverFundsFilter `json:"filter,omitempty"` // 查询过滤条件
}
// QueryAdverFundsResponse 查询广告金发放记录 响应参数
type QueryAdverFundsResponse struct {
util.CommonError
AdverFundsList []*AdverFundsRecord `json:"adver_funds_list"` // 发放记录列表
TotalPage int `json:"total_page"` // 总页数
}
// CreateFundsBillRequest 充值广告金,请求参数
type CreateFundsBillRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
TransferAmount int64 `json:"transfer_amount"` // 充值金额,单位分
TransferAccountUID int64 `json:"transfer_account_uid"` // 充值账户 UID
TransferAccountName string `json:"transfer_account_name"` // 充值账户名称
TransferAccountAgencyID int64 `json:"transfer_account_agency_id"` // 代理商 ID
RequestID string `json:"request_id"` // 幂等请求 ID,最长 1024 字符
SettleBegin int64 `json:"settle_begin"` // 结算周期开始时间,unix 秒级时间戳
SettleEnd int64 `json:"settle_end"` // 结算周期结束时间,unix 秒级时间戳
AuthorizeAdvertise int `json:"authorize_advertise"` // 是否授权广告数据 0-否 1-是
FundType int `json:"fund_type"` // 资金类型 0-普通赠送 1-广告激励 2-定向激励
}
// CreateFundsBillResponse 充值广告金 响应参数
type CreateFundsBillResponse struct {
util.CommonError
BillID string `json:"bill_id"` // 充值订单号
}
// BindTransferAccountRequest 绑定广告金充值账户,请求参数
type BindTransferAccountRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
TransferAccountUID int64 `json:"transfer_account_uid,omitempty"` // 充值账户 UID
TransferAccountOrgName string `json:"transfer_account_org_name,omitempty"` // 充值账户主体名称
}
// BindTransferAccountResponse 绑定广告金充值账户 响应参数
type BindTransferAccountResponse struct {
util.CommonError
}
// FundsBillFilter 广告金充值记录查询过滤条件
type FundsBillFilter struct {
OperTimeBegin int64 `json:"oper_time_begin"` // 充值开始时间,unix 秒级时间戳
OperTimeEnd int64 `json:"oper_time_end"` // 充值结束时间,unix 秒级时间戳
BillID string `json:"bill_id,omitempty"` // 充值订单号
RequestID string `json:"request_id,omitempty"` // 幂等请求 ID
}
// FundsBillRecord 广告金充值记录
type FundsBillRecord struct {
BillID string `json:"bill_id"` // 充值订单号
OperTime int64 `json:"oper_time"` // 充值时间,unix 秒级时间戳
SettleBegin int64 `json:"settle_begin"` // 对应广告金结算开始时间,unix 秒级时间戳
SettleEnd int64 `json:"settle_end"` // 对应广告金结算结束时间,unix 秒级时间戳
FundID string `json:"fund_id"` // 对应广告金发放 ID
TransferAccountName string `json:"transfer_account_name"` // 充值账户名称
TransferAccountUID int64 `json:"transfer_account_uid"` // 充值账户 UID
TransferAmount int64 `json:"transfer_amount"` // 充值金额,单位分
Status int `json:"status"` // 状态 0-充值中 1-成功 2-失败
RequestID string `json:"request_id"` // 幂等请求 ID
}
// QueryFundsBillRequest 查询广告金充值记录,请求参数
type QueryFundsBillRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
Page int `json:"page"` // 页码,>= 1
PageSize int `json:"page_size"` // 每页记录数
Filter FundsBillFilter `json:"filter"` // 查询过滤条件
}
// QueryFundsBillResponse 查询广告金充值记录 响应参数
type QueryFundsBillResponse struct {
util.CommonError
BillList []*FundsBillRecord `json:"bill_list"` // 充值记录列表
TotalPage int `json:"total_page"` // 总页数
}
// RecoverBillFilter 广告金回收记录查询过滤条件
type RecoverBillFilter struct {
RecoverTimeBegin int64 `json:"recover_time_begin"` // 回收开始时间,unix 秒级时间戳
RecoverTimeEnd int64 `json:"recover_time_end"` // 回收结束时间,unix 秒级时间戳
BillID string `json:"bill_id,omitempty"` // 回收订单号
}
// RecoverBillRecord 广告金回收记录
type RecoverBillRecord struct {
BillID string `json:"bill_id"` // 回收订单号
RecoverTime int64 `json:"recover_time"` // 回收时间,unix 秒级时间戳
SettleBegin int64 `json:"settle_begin"` // 结算周期开始时间,unix 秒级时间戳
SettleEnd int64 `json:"settle_end"` // 结算周期结束时间,unix 秒级时间戳
FundID string `json:"fund_id"` // 对应广告金发放 ID
RecoverAccountName string `json:"recover_account_name"` // 回收账户
RecoverAmount int64 `json:"recover_amount"` // 回收金额,单位分
RefundOrderList []string `json:"refund_order_list"` // 对应的退款订单号列表
}
// QueryRecoverBillRequest 查询广告金回收记录,请求参数
type QueryRecoverBillRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
Page int `json:"page"` // 页码,>= 1
PageSize int `json:"page_size"` // 每页记录数
Filter RecoverBillFilter `json:"filter"` // 查询过滤条件
}
// QueryRecoverBillResponse 查询广告金回收记录 响应参数
type QueryRecoverBillResponse struct {
util.CommonError
BillList []*RecoverBillRecord `json:"bill_list"` // 回收记录列表
TotalPage int `json:"total_page"` // 总页数
}
// DownloadAdverFundsOrderRequest 下载广告金对应的商户订单信息,请求参数
type DownloadAdverFundsOrderRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
FundID string `json:"fund_id"` // 广告金发放 ID
}
// DownloadAdverFundsOrderResponse 下载广告金对应的商户订单信息 响应参数
type DownloadAdverFundsOrderResponse struct {
util.CommonError
URL string `json:"url"` // 订单下载地址
}
// ==================== 投诉处理 ====================
// ComplaintOrderInfo 投诉关联订单信息
type ComplaintOrderInfo struct {
TransactionID string `json:"transaction_id"` // 微信支付交易单号
MchOrderNo string `json:"mch_order_no"` // 商户订单号
RefundID string `json:"refund_id"` // 退款订单号
}
// ComplaintMedia 投诉媒体信息
type ComplaintMedia struct {
MediaType int `json:"media_type"` // 媒体类型
MediaURL string `json:"media_url"` // 媒体 URL
MediaTags []string `json:"media_tags"` // 媒体标签
}
// ComplaintItem 投诉详情
type ComplaintItem struct {
ComplaintID string `json:"complaint_id"` // 投诉单号
ComplaintTime string `json:"complaint_time"` // 投诉时间
ComplaintDetail string `json:"complaint_detail"` // 投诉内容
ComplaintState string `json:"complaint_state"` // 投诉状态 PENDING/PROCESSING/PROCESSED
PayerPhone string `json:"payer_phone"` // 投诉人联系方式
PayerOpenID string `json:"payer_openid"` // 投诉人 OpenID
ComplaintOrderInfo []*ComplaintOrderInfo `json:"complaint_order_info"` // 关联订单信息
ComplaintFullRefunded bool `json:"complaint_full_refunded"` // 是否全部退款
IncomingUserResponse bool `json:"incoming_user_response"` // 是否有待处理的用户消息
UserComplaintTimes int `json:"user_complaint_times"` // 用户投诉次数
ComplaintMediaList []*ComplaintMedia `json:"complaint_media_list"` // 用户上传的证据
}
// GetComplaintListRequest 获取投诉列表,请求参数
type GetComplaintListRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
BeginDate string `json:"begin_date"` // 查询开始日期,格式 yyyy-mm-dd
EndDate string `json:"end_date"` // 查询结束日期,格式 yyyy-mm-dd
Offset int `json:"offset"` // 偏移量,从 0 开始
Limit int `json:"limit"` // 最大返回记录数
}
// GetComplaintListResponse 获取投诉列表 响应参数
type GetComplaintListResponse struct {
util.CommonError
Total int `json:"total"` // 总数
Complaints []*ComplaintItem `json:"complaints"` // 投诉列表
}
// GetComplaintDetailRequest 获取投诉详情,请求参数
type GetComplaintDetailRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
ComplaintID string `json:"complaint_id"` // 投诉单号
}
// GetComplaintDetailResponse 获取投诉详情 响应参数
type GetComplaintDetailResponse struct {
util.CommonError
Complaint *ComplaintItem `json:"complaint"` // 投诉详情
}
// NegotiationHistory 协商历史记录
type NegotiationHistory struct {
LogID string `json:"log_id"` // 操作流水号
Operator string `json:"operator"` // 操作人
OperateTime string `json:"operate_time"` // 操作时间
OperateType string `json:"operate_type"` // 操作类型
OperateDetails string `json:"operate_details"` // 操作详情
ComplaintMediaList []*ComplaintMedia `json:"complaint_media_list"` // 上传的证据
}
// GetNegotiationHistoryRequest 获取协商历史,请求参数
type GetNegotiationHistoryRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
ComplaintID string `json:"complaint_id"` // 投诉单号
Offset int `json:"offset"` // 偏移量,从 0 开始
Limit int `json:"limit"` // 最大返回记录数
}
// GetNegotiationHistoryResponse 获取协商历史 响应参数
type GetNegotiationHistoryResponse struct {
util.CommonError
Total int `json:"total"` // 总数
History []*NegotiationHistory `json:"history"` // 协商历史列表
}
// ResponseComplaintRequest 回复用户投诉,请求参数
type ResponseComplaintRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
ComplaintID string `json:"complaint_id"` // 投诉单号
ResponseContent string `json:"response_content"` // 回复内容
ResponseImages []string `json:"response_images"` // 图片文件 ID 列表(来自 upload_vp_file)
}
// ResponseComplaintResponse 回复用户投诉 响应参数
type ResponseComplaintResponse struct {
util.CommonError
}
// CompleteComplaintRequest 完成投诉处理,请求参数
type CompleteComplaintRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
ComplaintID string `json:"complaint_id"` // 投诉单号
}
// CompleteComplaintResponse 完成投诉处理 响应参数
type CompleteComplaintResponse struct {
util.CommonError
}
// UploadVPFileRequest 上传媒体文件,请求参数
type UploadVPFileRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
Base64Img string `json:"base64_img,omitempty"` // Base64 编码的图片,最大 1MB
ImgURL string `json:"img_url,omitempty"` // 图片 URL(可直接下载,不支持 302 跳转),最大 2MB,优先使用此字段
FileName string `json:"file_name"` // 图片名称
}
// UploadVPFileResponse 上传媒体文件 响应参数
type UploadVPFileResponse struct {
util.CommonError
FileID string `json:"file_id"` // 返回的文件 ID(用于 response_complaint)
}
// GetUploadFileSignRequest 获取微信支付反馈投诉图片的签名头部,请求参数
type GetUploadFileSignRequest struct {
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
WxpayURL string `json:"wxpay_url"` // 微信支付图片 URL,格式 "https://api.mch.weixin.qq.com/v3/merchant-service/images/{xxxxxx}"
ConvertCOS bool `json:"convert_cos"` // 是否转换为 COS,获取临时下载链接(有效期 30 分钟)
ComplaintID string `json:"complaint_id"` // 对应的投诉单号
}
// GetUploadFileSignResponse 获取微信支付反馈投诉图片的签名头部 响应参数
type GetUploadFileSignResponse struct {
util.CommonError
Sign string `json:"sign"` // Authorization 头部值
CosURL string `json:"cos_url"` // 当 convert_cos=true 时返回的 COS URL,有效期 30 分钟
}
// URLParams url parameter
type URLParams struct {
Path string `json:"path"`
+522 -28
View File
@@ -55,7 +55,7 @@ func (s *VirtualPayment) QueryUserBalance(ctx context.Context, in *QueryUserBala
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -83,7 +83,7 @@ func (s *VirtualPayment) CurrencyPay(ctx context.Context, in *CurrencyPayRequest
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -111,7 +111,7 @@ func (s *VirtualPayment) QueryOrder(ctx context.Context, in *QueryOrderRequest)
return
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -139,7 +139,7 @@ func (s *VirtualPayment) CancelCurrencyPay(ctx context.Context, in *CancelCurren
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -169,7 +169,7 @@ func (s *VirtualPayment) NotifyProvideGoods(ctx context.Context, in *NotifyProvi
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -198,7 +198,7 @@ func (s *VirtualPayment) PresentCurrency(ctx context.Context, in *PresentCurrenc
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -227,7 +227,7 @@ func (s *VirtualPayment) DownloadBill(ctx context.Context, in *DownloadBillReque
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -256,7 +256,7 @@ func (s *VirtualPayment) RefundOrder(ctx context.Context, in *RefundOrderRequest
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -285,7 +285,7 @@ func (s *VirtualPayment) CreateWithdrawOrder(ctx context.Context, in *CreateWith
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -314,7 +314,7 @@ func (s *VirtualPayment) QueryWithdrawOrder(ctx context.Context, in *QueryWithdr
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -343,7 +343,7 @@ func (s *VirtualPayment) StartUploadGoods(ctx context.Context, in *StartUploadGo
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -372,7 +372,7 @@ func (s *VirtualPayment) QueryUploadGoods(ctx context.Context, in *QueryUploadGo
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -401,7 +401,7 @@ func (s *VirtualPayment) StartPublishGoods(ctx context.Context, in *StartPublish
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -430,7 +430,7 @@ func (s *VirtualPayment) QueryPublishGoods(ctx context.Context, in *QueryPublish
}
var response []byte
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
@@ -439,6 +439,499 @@ func (s *VirtualPayment) QueryPublishGoods(ctx context.Context, in *QueryPublish
return
}
// StartDownloadOrder 发起下载小程序订单明细任务
func (s *VirtualPayment) StartDownloadOrder(ctx context.Context, in *StartDownloadOrderRequest) (out StartDownloadOrderResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: startDownloadOrder,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "StartDownloadOrder")
return
}
// QueryDownloadOrder 查询下载订单任务结果
func (s *VirtualPayment) QueryDownloadOrder(ctx context.Context, in *QueryDownloadOrderRequest) (out QueryDownloadOrderResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: queryDownloadOrder,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "QueryDownloadOrder")
return
}
// QueryBizBalance 查询商家账户可提现余额
func (s *VirtualPayment) QueryBizBalance(ctx context.Context, in *QueryBizBalanceRequest) (out QueryBizBalanceResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: queryBizBalance,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "QueryBizBalance")
return
}
// QueryTransferAccount 查询广告金充值账户
func (s *VirtualPayment) QueryTransferAccount(ctx context.Context, in *QueryTransferAccountRequest) (out QueryTransferAccountResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: queryTransferAccount,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "QueryTransferAccount")
return
}
// QueryAdverFunds 查询广告金发放记录
func (s *VirtualPayment) QueryAdverFunds(ctx context.Context, in *QueryAdverFundsRequest) (out QueryAdverFundsResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: queryAdverFunds,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "QueryAdverFunds")
return
}
// CreateFundsBill 充值广告金
func (s *VirtualPayment) CreateFundsBill(ctx context.Context, in *CreateFundsBillRequest) (out CreateFundsBillResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: createFundsBill,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "CreateFundsBill")
return
}
// BindTransferAccount 绑定广告金充值账户
func (s *VirtualPayment) BindTransferAccount(ctx context.Context, in *BindTransferAccountRequest) (out BindTransferAccountResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: bindTransferAccount,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "BindTransferAccount")
return
}
// QueryFundsBill 查询广告金充值记录
func (s *VirtualPayment) QueryFundsBill(ctx context.Context, in *QueryFundsBillRequest) (out QueryFundsBillResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: queryFundsBill,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "QueryFundsBill")
return
}
// QueryRecoverBill 查询广告金回收记录
func (s *VirtualPayment) QueryRecoverBill(ctx context.Context, in *QueryRecoverBillRequest) (out QueryRecoverBillResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: queryRecoverBill,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "QueryRecoverBill")
return
}
// DownloadAdverFundsOrder 下载广告金对应的商户订单信息
func (s *VirtualPayment) DownloadAdverFundsOrder(ctx context.Context, in *DownloadAdverFundsOrderRequest) (out DownloadAdverFundsOrderResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: downloadAdverFundsOrder,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "DownloadAdverFundsOrder")
return
}
// GetComplaintList 获取投诉列表
func (s *VirtualPayment) GetComplaintList(ctx context.Context, in *GetComplaintListRequest) (out GetComplaintListResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: getComplaintList,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "GetComplaintList")
return
}
// GetComplaintDetail 获取投诉详情
func (s *VirtualPayment) GetComplaintDetail(ctx context.Context, in *GetComplaintDetailRequest) (out GetComplaintDetailResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: getComplaintDetail,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "GetComplaintDetail")
return
}
// GetNegotiationHistory 获取协商历史
func (s *VirtualPayment) GetNegotiationHistory(ctx context.Context, in *GetNegotiationHistoryRequest) (out GetNegotiationHistoryResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: getNegotiationHistory,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "GetNegotiationHistory")
return
}
// ResponseComplaint 回复用户
func (s *VirtualPayment) ResponseComplaint(ctx context.Context, in *ResponseComplaintRequest) (out ResponseComplaintResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: responseComplaint,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "ResponseComplaint")
return
}
// CompleteComplaint 完成投诉处理
func (s *VirtualPayment) CompleteComplaint(ctx context.Context, in *CompleteComplaintRequest) (out CompleteComplaintResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: completeComplaint,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "CompleteComplaint")
return
}
// UploadVPFile 上传媒体文件
func (s *VirtualPayment) UploadVPFile(ctx context.Context, in *UploadVPFileRequest) (out UploadVPFileResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: uploadVPFile,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "UploadVPFile")
return
}
// GetUploadFileSign 获取微信支付反馈投诉图片的签名头部
func (s *VirtualPayment) GetUploadFileSign(ctx context.Context, in *GetUploadFileSignRequest) (out GetUploadFileSignResponse, err error) {
var jsonByte []byte
if jsonByte, err = json.Marshal(in); err != nil {
return
}
var (
params = URLParams{
Path: getUploadFileSign,
Content: string(jsonByte),
Signature: EmptyString,
}
address string
)
if address, err = s.requestAddress(params); err != nil {
return
}
var response []byte
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
return
}
// 使用通用方法返回错误
err = util.DecodeWithError(response, &out, "GetUploadFileSign")
return
}
// hmacSha256 hmac sha256
func (s *VirtualPayment) hmacSha256(key, data string) string {
h := hmac.New(sha256.New, []byte(key))
@@ -446,6 +939,13 @@ func (s *VirtualPayment) hmacSha256(key, data string) string {
return hex.EncodeToString(h.Sum(nil))
}
// postJSONBytesContext POST JSON bytes with context
func postJSONBytesContext(ctx context.Context, address string, jsonByte []byte) ([]byte, error) {
return util.HTTPPostContext(ctx, address, jsonByte, map[string]string{
"Content-Type": "application/json;charset=utf-8",
})
}
// PaySign pay sign
func (s *VirtualPayment) PaySign(url, data string) (string, error) {
if strings.TrimSpace(s.ctx.Config.AppKey) == "" {
@@ -476,23 +976,17 @@ func (s *VirtualPayment) PaySignature(url, data string) (paySign, signature stri
// requestURL .组合 URL
func (s *VirtualPayment) requestAddress(params URLParams) (url string, err error) {
switch params.Path {
case queryUserBalance:
case currencyPay:
case cancelCurrencyPay:
case queryUserBalance, currencyPay, cancelCurrencyPay:
if params.PaySign, params.Signature, err = s.PaySignature(params.Path, params.Content); err != nil {
return
}
case queryOrder:
case notifyProvideGoods:
case presentCurrency:
case downloadBill:
case refundOrder:
case createWithdrawOrder:
case queryWithdrawOrder:
case startUploadGoods:
case queryUploadGoods:
case startPublishGoods:
case queryPublishGoods:
case queryOrder, notifyProvideGoods, presentCurrency, downloadBill, refundOrder,
createWithdrawOrder, queryWithdrawOrder, startUploadGoods, queryUploadGoods,
startPublishGoods, queryPublishGoods, startDownloadOrder, queryDownloadOrder,
queryBizBalance, queryTransferAccount, queryAdverFunds, createFundsBill,
bindTransferAccount, queryFundsBill, queryRecoverBill, downloadAdverFundsOrder,
getComplaintList, getComplaintDetail, getNegotiationHistory, responseComplaint,
completeComplaint, uploadVPFile, getUploadFileSign:
if params.PaySign, err = s.PaySign(params.Path, params.Content); err != nil {
return
}
+23 -2
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
+1 -1
View File
@@ -2,7 +2,7 @@ package account
import "github.com/silenceper/wechat/v2/openplatform/context"
// Account 开放平台张哈管理
// Account 开放平台帐号管理
// TODO 实现方法
type Account struct {
*context.Context
+30 -1
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
}
+22 -3
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
+147
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)
}
})
}
+15
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
}
+15 -1
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
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")
}
+10
View File
@@ -6,6 +6,8 @@ import (
log "github.com/sirupsen/logrus"
"github.com/silenceper/wechat/v2/aispeech"
aispeechConfig "github.com/silenceper/wechat/v2/aispeech/config"
"github.com/silenceper/wechat/v2/cache"
"github.com/silenceper/wechat/v2/miniprogram"
miniConfig "github.com/silenceper/wechat/v2/miniprogram/config"
@@ -84,6 +86,14 @@ func (wc *Wechat) GetWork(cfg *workConfig.Config) *work.Work {
return work.NewWork(cfg)
}
// GetAISpeech 获取微信智能对话的实例
func (wc *Wechat) GetAISpeech(cfg *aispeechConfig.Config) *aispeech.AISpeech {
if cfg.Cache == nil {
cfg.Cache = wc.cache
}
return aispeech.NewAISpeech(cfg)
}
// SetHTTPClient 设置HTTPClient
func (wc *Wechat) SetHTTPClient(client *http.Client) {
util.DefaultHTTPClient = client
+190 -11
View File
@@ -24,6 +24,18 @@ const (
convertToOpenIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid"
// convertToUserIDURL openID转userID
convertToUserIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_userid"
// userBatchDeleteURL 批量删除成员
userBatchDeleteURL = "https://qyapi.weixin.qq.com/cgi-bin/user/batchdelete?access_token=%s"
// userAuthSuccURL 登录二次验证
userAuthSuccURL = "https://qyapi.weixin.qq.com/cgi-bin/user/authsucc?access_token=%s&userid=%s"
// batchInviteURL 邀请成员
batchInviteURL = "https://qyapi.weixin.qq.com/cgi-bin/batch/invite?access_token=%s"
// getJoinQrcodeURL 获取加入企业二维码
getJoinQrcodeURL = "https://qyapi.weixin.qq.com/cgi-bin/corp/get_join_qrcode"
// getUseridURL 手机号获取userid
getUseridURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?access_token=%s"
// getUseridByEmailURL 邮箱获取userid
getUseridByEmailURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get_userid_by_email?access_token=%s"
)
type (
@@ -158,17 +170,20 @@ func (r *Client) UserCreate(req *UserCreateRequest) (*UserCreateResponse, error)
// UserUpdateRequest 更新成员请求
type UserUpdateRequest struct {
UserID string `json:"userid"`
NewUserID string `json:"new_userid"`
Name string `json:"name"`
Alias string `json:"alias"`
Mobile string `json:"mobile"`
Department []int `json:"department"`
Order []int `json:"order"`
Position string `json:"position"`
Gender int `json:"gender"`
Email string `json:"email"`
BizMail string `json:"biz_mail"`
UserID string `json:"userid"`
NewUserID string `json:"new_userid"`
Name string `json:"name"`
Alias string `json:"alias"`
Mobile string `json:"mobile"`
Department []int `json:"department"`
Order []int `json:"order"`
Position string `json:"position"`
Gender int `json:"gender"`
Email string `json:"email"`
BizMail string `json:"biz_mail"`
BizMailAlias struct {
Item []string `json:"item"`
} `json:"biz_mail_alias"`
IsLeaderInDept []int `json:"is_leader_in_dept"`
DirectLeader []string `json:"direct_leader"`
Enable int `json:"enable"`
@@ -444,3 +459,167 @@ func (r *Client) ConvertToUserID(openID string) (string, error) {
err = util.DecodeWithError(response, result, "ConvertToUserID")
return result.UserID, err
}
// UserBatchDeleteRequest 批量删除成员请求
type UserBatchDeleteRequest struct {
UseridList []string `json:"useridlist"`
}
// UserBatchDelete 批量删除成员
// see https://developer.work.weixin.qq.com/document/path/90199
func (r *Client) UserBatchDelete(req *UserBatchDeleteRequest) 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(userBatchDeleteURL, accessToken), req); err != nil {
return err
}
return util.DecodeWithCommonError(response, "UserBatchDelete")
}
// UserAuthSucc 登录二次验证
// @see https://developer.work.weixin.qq.com/document/path/90203
func (r *Client) UserAuthSucc(userID string) error {
var (
accessToken string
err error
)
if accessToken, err = r.GetAccessToken(); err != nil {
return err
}
var response []byte
if response, err = util.HTTPGet(fmt.Sprintf(userAuthSuccURL, accessToken, userID)); err != nil {
return err
}
return util.DecodeWithCommonError(response, "UserAuthSucc")
}
// BatchInviteRequest 邀请成员请求
type BatchInviteRequest struct {
User []string `json:"user"`
Party []int `json:"party"`
Tag []int `json:"tag"`
}
// BatchInviteResponse 邀请成员响应
type BatchInviteResponse struct {
util.CommonError
InvalidUser []string `json:"invaliduser"`
InvalidParty []int `json:"invalidparty"`
InvalidTag []int `json:"invalidtag"`
}
// BatchInvite 邀请成员
// see https://developer.work.weixin.qq.com/document/path/90975
func (r *Client) BatchInvite(req *BatchInviteRequest) (*BatchInviteResponse, 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(batchInviteURL, accessToken), req); err != nil {
return nil, err
}
result := &BatchInviteResponse{}
err = util.DecodeWithError(response, result, "BatchInvite")
return result, err
}
// GetJoinQrcodeRequest 获取加入企业二维码请求
type GetJoinQrcodeRequest struct {
SizeType int `json:"size_type"`
}
// GetJoinQrcodeResponse 获取加入企业二维码响应
type GetJoinQrcodeResponse struct {
util.CommonError
JoinQrcode string `json:"join_qrcode"`
}
// GetJoinQrcode 获取加入企业二维码
// see https://developer.work.weixin.qq.com/document/path/91714
func (r *Client) GetJoinQrcode(req *GetJoinQrcodeRequest) (*GetJoinQrcodeResponse, error) {
var (
accessToken string
err error
apiURL string
)
if accessToken, err = r.GetAccessToken(); err != nil {
return nil, err
}
if req.SizeType > 0 {
apiURL = fmt.Sprintf("%s?access_token=%s&size_type=%d", getJoinQrcodeURL, accessToken, req.SizeType)
} else {
apiURL = fmt.Sprintf("%s?access_token=%s", getJoinQrcodeURL, accessToken)
}
var response []byte
if response, err = util.HTTPGet(apiURL); err != nil {
return nil, err
}
result := &GetJoinQrcodeResponse{}
err = util.DecodeWithError(response, result, "GetJoinQrcode")
return result, err
}
// GetUseridRequest 手机号获取userid请求
type GetUseridRequest struct {
Mobile string `json:"mobile"`
}
// GetUseridResponse 获取userid响应
type GetUseridResponse struct {
util.CommonError
Userid string `json:"userid"`
}
// GetUserid 手机号获取userid
// see https://developer.work.weixin.qq.com/document/path/95402
func (r *Client) GetUserid(req *GetUseridRequest) (*GetUseridResponse, 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(getUseridURL, accessToken), req); err != nil {
return nil, err
}
result := &GetUseridResponse{}
err = util.DecodeWithError(response, result, "GetUserid")
return result, err
}
// GetUseridByEmailRequest 邮箱获取userid请求
type GetUseridByEmailRequest struct {
Email string `json:"email"`
EmailType int `json:"email_type,omitempty"`
}
// GetUseridByEmail 邮箱获取userid
// see https://developer.work.weixin.qq.com/document/path/95895
func (r *Client) GetUseridByEmail(req *GetUseridByEmailRequest) (*GetUseridResponse, 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(getUseridByEmailURL, accessToken), req); err != nil {
return nil, err
}
result := &GetUseridResponse{}
err = util.DecodeWithError(response, result, "GetUseridByEmail")
return result, err
}
+70 -14
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"`
}
@@ -155,24 +158,32 @@ type OptionGroupRuleCheckinDate struct {
MaxAllowArriveEarly int64 `json:"max_allow_arrive_early"`
MaxAllowArriveLate int64 `json:"max_allow_arrive_late"`
LateRule OptionGroupLateRule `json:"late_rule"`
Biweekly OptionGroupBiweekly `json:"biweekly,omitempty"`
}
// OptionGroupRuleCheckinTime 工作日上下班打卡时间信息
type OptionGroupRuleCheckinTime struct {
TimeID int64 `json:"time_id"`
WorkSec int64 `json:"work_sec"`
OffWorkSec int64 `json:"off_work_sec"`
RemindWorkSec int64 `json:"remind_work_sec"`
RemindOffWorkSec int64 `json:"remind_off_work_sec"`
AllowRest bool `json:"allow_rest"`
RestBeginTime int64 `json:"rest_begin_time"`
RestEndTime int64 `json:"rest_end_time"`
EarliestWorkSec int64 `json:"earliest_work_sec"`
LatestWorkSec int64 `json:"latest_work_sec"`
EarliestOffWorkSec int64 `json:"earliest_off_work_sec"`
LatestOffWorkSec int64 `json:"latest_off_work_sec"`
NoNeedCheckOn bool `json:"no_need_checkon"`
NoNeedCheckOff bool `json:"no_need_checkoff"`
TimeID int64 `json:"time_id"`
WorkSec int64 `json:"work_sec"`
OffWorkSec int64 `json:"off_work_sec"`
RemindWorkSec int64 `json:"remind_work_sec"`
RemindOffWorkSec int64 `json:"remind_off_work_sec"`
AllowRest bool `json:"allow_rest"`
RestBeginTime int64 `json:"rest_begin_time"`
RestEndTime int64 `json:"rest_end_time"`
EarliestWorkSec int64 `json:"earliest_work_sec"`
LatestWorkSec int64 `json:"latest_work_sec"`
EarliestOffWorkSec int64 `json:"earliest_off_work_sec"`
LatestOffWorkSec int64 `json:"latest_off_work_sec"`
NoNeedCheckOn bool `json:"no_need_checkon"`
NoNeedCheckOff bool `json:"no_need_checkoff"`
RestTimes []OptionGroupRuleRestTimes `json:"rest_times,omitempty"`
}
// OptionGroupRuleRestTimes 多组休息时间
type OptionGroupRuleRestTimes struct {
RestBeginTime int64 `json:"rest_begin_time,omitempty"`
RestEndTime int64 `json:"rest_end_time,omitempty"`
}
// OptionGroupLateRule 晚走晚到时间规则信息
@@ -189,6 +200,13 @@ type OptionGroupTimeRule struct {
OnWorkFlexTime int64 `json:"onwork_flex_time"`
}
// OptionGroupBiweekly 大小周规则
type OptionGroupBiweekly struct {
EnableWeekdayRecurrence bool `json:"enable_weekday_recurrence"`
OddWorkdays []int64 `json:"odd_workdays"`
EvenWorkdays []int64 `json:"even_workdays"`
}
// OptionGroupSpeWorkdays 特殊工作日
type OptionGroupSpeWorkdays struct {
Timestamp int64 `json:"timestamp"`
@@ -385,3 +403,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")
}
+28 -6
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 打卡时间配置
+29
View File
@@ -17,6 +17,8 @@ const (
listContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list_contact_way?access_token=%s"
// delContactWayURL 删除企业已配置的「联系我」方式
delContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/del_contact_way?access_token=%s"
// closeTempChatURL 结束临时会话
closeTempChatURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/close_temp_chat?access_token=%s"
)
type (
@@ -77,6 +79,8 @@ type (
ExpiresIn int `json:"expires_in"`
ChatExpiresIn int `json:"chat_expires_in"`
UnionID string `json:"unionid"`
IsExclusive bool `json:"is_exclusive"`
MarkSource bool `json:"mark_source"`
Conclusions ConclusionsRequest `json:"conclusions"`
}
// AddContactWayResponse 配置客户联系「联系我」方式响应
@@ -132,6 +136,7 @@ type (
ExpiresIn int `json:"expires_in"`
ChatExpiresIn int `json:"chat_expires_in"`
UnionID string `json:"unionid"`
MarkSource bool `json:"mark_source"`
Conclusions ConclusionsResponse `json:"conclusions"`
}
)
@@ -168,6 +173,7 @@ type (
ExpiresIn int `json:"expires_in"`
ChatExpiresIn int `json:"chat_expires_in"`
UnionID string `json:"unionid"`
MarkSource bool `json:"mark_source"`
Conclusions ConclusionsRequest `json:"conclusions"`
}
// UpdateContactWayResponse 更新企业已配置的「联系我」方式响应
@@ -263,3 +269,26 @@ func (r *Client) DelContactWay(req *DelContactWayRequest) (*DelContactWayRespons
err = util.DecodeWithError(response, result, "DelContactWay")
return result, err
}
// CloseTempChatRequest 结束临时会话请求
type CloseTempChatRequest struct {
UserID string `json:"userid"`
ExternalUserID string `json:"external_userid"`
}
// CloseTempChat 结束临时会话
// @see https://developer.work.weixin.qq.com/document/path/92228
func (r *Client) CloseTempChat(req *CloseTempChatRequest) 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(closeTempChatURL, accessToken), req); err != nil {
return err
}
return util.DecodeWithCommonError(response, "CloseTempChat")
}
+93 -10
View File
@@ -23,6 +23,10 @@ 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"
// customerAcquisitionGetPermitURL 获取客户可建联成员
customerAcquisitionGetPermitURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition_app/get_permit?access_token=%s"
)
type (
@@ -66,9 +70,10 @@ type (
// GetCustomerAcquisitionResponse 获取获客链接详情响应
GetCustomerAcquisitionResponse struct {
util.CommonError
Link Link `json:"link"`
Range CustomerAcquisitionRange `json:"range"`
SkipVerify bool `json:"skip_verify"`
Link Link `json:"link"`
Range CustomerAcquisitionRange `json:"range"`
PriorityOption CustomerPriorityOption `json:"priority_option"`
SkipVerify bool `json:"skip_verify"`
}
// Link 获客链接
Link struct {
@@ -76,6 +81,8 @@ type (
LinkName string `json:"link_name"`
URL string `json:"url"`
CreateTime int64 `json:"create_time"`
SkipVerify bool `json:"skip_verify"`
MarkSource bool `json:"mark_source"`
}
// CustomerAcquisitionRange 该获客链接使用范围
@@ -83,6 +90,12 @@ type (
UserList []string `json:"user_list"`
DepartmentList []int64 `json:"department_list"`
}
// CustomerPriorityOption 该获客链接的优先选项
CustomerPriorityOption struct {
PriorityType int `json:"priority_type"`
PriorityUseridList []string `json:"priority_userid_list"`
}
)
// GetCustomerAcquisition 获客助手--获取获客链接详情
@@ -107,9 +120,11 @@ func (r *Client) GetCustomerAcquisition(req *GetCustomerAcquisitionRequest) (*Ge
type (
// CreateCustomerAcquisitionLinkRequest 创建获客链接请求
CreateCustomerAcquisitionLinkRequest struct {
LinkName string `json:"link_name"`
Range CustomerAcquisitionRange `json:"range"`
SkipVerify bool `json:"skip_verify"`
LinkName string `json:"link_name"`
Range CustomerAcquisitionRange `json:"range"`
SkipVerify bool `json:"skip_verify"`
PriorityOption CustomerPriorityOption `json:"priority_option"`
MarkSource bool `json:"mark_source"`
}
// CreateCustomerAcquisitionLinkResponse 创建获客链接响应
CreateCustomerAcquisitionLinkResponse struct {
@@ -140,10 +155,12 @@ func (r *Client) CreateCustomerAcquisitionLink(req *CreateCustomerAcquisitionLin
type (
// UpdateCustomerAcquisitionLinkRequest 编辑获客链接请求
UpdateCustomerAcquisitionLinkRequest struct {
LinkID string `json:"link_id"`
LinkName string `json:"link_name"`
Range CustomerAcquisitionRange `json:"range"`
SkipVerify bool `json:"skip_verify"`
LinkID string `json:"link_id"`
LinkName string `json:"link_name"`
Range CustomerAcquisitionRange `json:"range"`
SkipVerify bool `json:"skip_verify"`
PriorityOption CustomerPriorityOption `json:"priority_option"`
MarkSource bool `json:"mark_source"`
}
// UpdateCustomerAcquisitionLinkResponse 编辑获客链接响应
UpdateCustomerAcquisitionLinkResponse struct {
@@ -308,3 +325,69 @@ 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
}
// GetPermitResponse 获取客户可建联成员响应
type GetPermitResponse struct {
util.CommonError
UserList []string `json:"user_list"`
DepartmentList []int `json:"department_list"`
TagList []int `json:"tag_list"`
}
// GetPermit 获取客户可建联成员
// see https://developer.work.weixin.qq.com/document/path/101146
func (r *Client) GetPermit() (*GetPermitResponse, error) {
var (
accessToken string
err error
)
if accessToken, err = r.GetAccessToken(); err != nil {
return nil, err
}
var response []byte
if response, err = util.HTTPGet(fmt.Sprintf(customerAcquisitionGetPermitURL, accessToken)); err != nil {
return nil, err
}
result := &GetPermitResponse{}
err = util.DecodeWithError(response, result, "CustomerAcquisitionGetPermit")
return result, err
}
+16 -15
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 批量获取外部联系人客户列表
@@ -186,16 +187,16 @@ type ExternalUserForBatch struct {
// ExternalContact 批量获取外部联系人用户信息
type ExternalContact struct {
ExternalUserID string `json:"external_userid"`
Name string `json:"name"`
Position string `json:"position"`
Avatar string `json:"avatar"`
CorpName string `json:"corp_name"`
CorpFullName string `json:"corp_full_name"`
Type int64 `json:"type"`
Gender int64 `json:"gender"`
UnionID string `json:"unionid"`
ExternalProfile string `json:"external_profile"`
ExternalUserID string `json:"external_userid"`
Name string `json:"name"`
Position string `json:"position"`
Avatar string `json:"avatar"`
CorpName string `json:"corp_name"`
CorpFullName string `json:"corp_full_name"`
Type int64 `json:"type"`
Gender int64 `json:"gender"`
UnionID string `json:"unionid"`
ExternalProfile *ExternalProfile `json:"external_profile"`
}
// FollowInfo 批量获取外部联系人跟进人信息
@@ -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 修改客户备注信息请求体
+3
View File
@@ -19,6 +19,7 @@ type (
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
MarkSource bool `json:"mark_source"`
}
// AddJoinWayResponse 添加群配置返回值
@@ -65,6 +66,7 @@ type (
ChatIDList []string `json:"chat_id_list"`
QrCode string `json:"qr_code"`
State string `json:"state"`
MarkSource bool `json:"mark_source"`
}
//GetJoinWayResponse 获取群配置的返回值
GetJoinWayResponse struct {
@@ -103,6 +105,7 @@ type UpdateJoinWayRequest struct {
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
MarkSource bool `json:"mark_source"`
}
// UpdateJoinWay 更新客户群进群方式配置
+4 -4
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 验证请求参数是否合法并返回解密后的消息内容
+2 -8
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")
}
+58
View File
@@ -27,6 +27,10 @@ var (
getUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=%s&code=%s"
// getUserDetailURL 获取访问用户敏感信息
getUserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=%s"
// getTfaInfoURL 获取用户二次验证信息
getTfaInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/get_tfa_info?access_token=%s"
// tfaSuccURL 使用二次验证
tfaSuccURL = "https://qyapi.weixin.qq.com/cgi-bin/user/tfa_succ?access_token=%s"
)
// NewOauth new init oauth
@@ -163,3 +167,57 @@ func (ctr *Oauth) GetUserDetail(req *GetUserDetailRequest) (*GetUserDetailRespon
err = util.DecodeWithError(response, result, "GetUserDetail")
return result, err
}
// GetTfaInfoRequest 获取用户二次验证信息请求
type GetTfaInfoRequest struct {
Code string `json:"code"`
}
// GetTfaInfoResponse 获取用户二次验证信息响应
type GetTfaInfoResponse struct {
util.CommonError
UserID string `json:"userid"`
TfaCode string `json:"tfa_code"`
}
// GetTfaInfo 获取用户二次验证信息
// @see https://developer.work.weixin.qq.com/document/path/99499
func (ctr *Oauth) GetTfaInfo(req *GetTfaInfoRequest) (*GetTfaInfoResponse, error) {
var (
accessToken string
err error
)
if accessToken, err = ctr.GetAccessToken(); err != nil {
return nil, err
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(getTfaInfoURL, accessToken), req); err != nil {
return nil, err
}
result := &GetTfaInfoResponse{}
err = util.DecodeWithError(response, result, "GetTfaInfo")
return result, err
}
// TfaSuccRequest 使用二次验证请求
type TfaSuccRequest struct {
UserID string `json:"userid"`
TfaCode string `json:"tfa_code"`
}
// TfaSucc 使用二次验证
// @see https://developer.work.weixin.qq.com/document/path/99500
func (ctr *Oauth) TfaSucc(req *TfaSuccRequest) error {
var (
accessToken string
err error
)
if accessToken, err = ctr.GetAccessToken(); err != nil {
return err
}
var response []byte
if response, err = util.PostJSON(fmt.Sprintf(tfaSuccURL, accessToken), req); err != nil {
return err
}
return util.DecodeWithCommonError(response, "TfaSucc")
}