Jelajahi Sumber

Merge branch 'v2' of github.com:silenceper/wechat into feature/remove-redis

houseme 2 tahun lalu
induk
melakukan
dad35b9f6d
35 mengubah file dengan 893 tambahan dan 217 penghapusan
  1. 3 3
      .github/ISSUE_TEMPLATE/bug.md
  2. 4 4
      .github/ISSUE_TEMPLATE/feature.md
  3. 3 3
      .github/ISSUE_TEMPLATE/question.md
  4. 9 9
      .github/workflows/go.yml
  5. 5 7
      credential/default_access_token.go
  6. 2 4
      miniprogram/auth/auth.go
  7. 1 6
      miniprogram/business/phone_number.go
  8. 2 2
      miniprogram/message/message.go
  9. 102 0
      miniprogram/message/updatable_msg.go
  10. 11 0
      miniprogram/miniprogram.go
  11. 4 14
      miniprogram/privacy/privacy.go
  12. 59 0
      miniprogram/redpacketcover/redpacketcover.go
  13. 2 12
      miniprogram/security/security.go
  14. 1 5
      miniprogram/shortlink/shortlink.go
  15. 1 5
      miniprogram/subscribe/subscribe.go
  16. 1 4
      miniprogram/urllink/urllink.go
  17. 2 6
      miniprogram/urlscheme/query.go
  18. 1 4
      miniprogram/urlscheme/urlscheme.go
  19. 2 5
      officialaccount/basic/short_url.go
  20. 12 2
      officialaccount/broadcast/broadcast.go
  21. 2 10
      officialaccount/customerservice/manager.go
  22. 0 3
      officialaccount/datacube/publisher.go
  23. 3 17
      officialaccount/draft/draft.go
  24. 2 12
      officialaccount/freepublish/freepublish.go
  25. 5 26
      officialaccount/message/subscribe.go
  26. 2 10
      officialaccount/message/template.go
  27. 0 4
      officialaccount/user/migrate.go
  28. 1 7
      officialaccount/user/tag.go
  29. 2 5
      openplatform/context/accessToken.go
  30. 184 0
      openplatform/miniprogram/basic/basic.go
  31. 13 11
      util/http.go
  32. 7 0
      wechat.go
  33. 387 0
      work/checkin/checkin.go
  34. 50 10
      work/externalcontact/external_user.go
  35. 8 7
      work/externalcontact/groupchat.go

+ 3 - 3
.github/ISSUE_TEMPLATE/bug.md

@@ -1,6 +1,6 @@
 ---
 ---
-name: 报告Bug
-about: 反馈BUG信息
+name: 报告 Bug
+about: 反馈 BUG 信息
 title: "[BUG]"
 title: "[BUG]"
 labels: bug
 labels: bug
 assignees: ''
 assignees: ''
@@ -18,4 +18,4 @@ assignees: ''
 
 
 
 
 **使用的版本**
 **使用的版本**
- - SDK版本: [比如 v0.0.0]
+ - SDK 版本:[比如 v0.0.0]

+ 4 - 4
.github/ISSUE_TEMPLATE/feature.md

@@ -1,6 +1,6 @@
 ---
 ---
-name: API需求
-about: 待实现的API接口,SDK的强大离不开社区的帮助,欢迎为项目贡献PR
+name: API 需求
+about: 待实现的 API 接口,SDK 的强大离不开社区的帮助,欢迎为项目贡献 PR
 title: "[Feature]"
 title: "[Feature]"
 labels: enhancement
 labels: enhancement
 assignees: ''
 assignees: ''
@@ -8,8 +8,8 @@ assignees: ''
 ---
 ---
 
 
 <!--
 <!--
-!!!SDK的强大离不开社区的帮助,欢迎为本项目贡献PR!!!
+!!!SDK 的强大离不开社区的帮助,欢迎为本项目贡献 PR!!!
 -->
 -->
-**你想要实现的模块或API**
+**你想要实现的模块或 API**
 
 
 
 

+ 3 - 3
.github/ISSUE_TEMPLATE/question.md

@@ -1,6 +1,6 @@
 ---
 ---
 name: 使用咨询
 name: 使用咨询
-about: 关于SDK使用相关的咨询,在使用前请先阅读官方微信文档
+about: 关于 SDK 使用相关的咨询,在使用前请先阅读官方微信文档
 title: "[咨询]"
 title: "[咨询]"
 labels: question
 labels: question
 assignees: ''
 assignees: ''
@@ -9,7 +9,7 @@ assignees: ''
 
 
 <!--
 <!--
 重要:
 重要:
-1、在使用本SDK前请先阅读对应的官方微信API文档:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
-2、本SDK部分接口文档: https://silenceper.com/wechat/
+1、在使用本 SDK 前请先阅读对应的官方微信 API 文档:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
+2、本 SDK 部分接口文档:https://silenceper.com/wechat/
 -->
 -->
 **请描述您的问题**
 **请描述您的问题**

+ 9 - 9
.github/workflows/go.yml

@@ -2,26 +2,26 @@ name: Go
 
 
 on:
 on:
   push:
   push:
-    branches: [ master,release-*,v2,feature/** ]
+    branches: [ master,release-*,v2,feature/**,fix/** ]
   pull_request:
   pull_request:
-    branches: [ master,release-*,v2,feature/** ]
+    branches: [ master,release-*,v2,feature/**,fix/** ]
 
 
 jobs:
 jobs:
   golangci:
   golangci:
     strategy:
     strategy:
       matrix:
       matrix:
-        go-version: [ '1.16','1.17','1.18','1.19','1.20','1.21' ]
+        go-version:  [ '1.16','1.17','1.18','1.19','1.20','1.21.4' ]
     name: golangci-lint
     name: golangci-lint
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - name: Setup Golang ${{ matrix.go-version }}
       - name: Setup Golang ${{ matrix.go-version }}
-        uses: actions/setup-go@v4
+        uses: actions/setup-go@v5
         with:
         with:
           go-version: ${{ matrix.go-version }}
           go-version: ${{ matrix.go-version }}
       - name: Checkout
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: golangci-lint
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v3
+        uses: golangci/golangci-lint-action@v4
         with:
         with:
           # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
           # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
           version: v1.52.2
           version: v1.52.2
@@ -37,12 +37,12 @@ jobs:
     # strategy set
     # strategy set
     strategy:
     strategy:
       matrix:
       matrix:
-        go: [ '1.16','1.17','1.18','1.19','1.20','1.21' ]
+        go: [ '1.16','1.17','1.18','1.19','1.20','1.21','1.22' ]
 
 
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - name: Set up Go 1.x
       - name: Set up Go 1.x
-        uses: actions/setup-go@v4
+        uses: actions/setup-go@v5
         with:
         with:
           go-version: ${{ matrix.go }}
           go-version: ${{ matrix.go }}
         id: go
         id: go

+ 5 - 7
credential/default_access_token.go

@@ -90,9 +90,9 @@ func (ak *DefaultAccessToken) GetAccessTokenContext(ctx context.Context) (access
 		return
 		return
 	}
 	}
 
 
-	if err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(resAccessToken.ExpiresIn-1500)*time.Second); err != nil {
-		return
-	}
+	expires := resAccessToken.ExpiresIn - 1500
+	err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
+
 	accessToken = resAccessToken.AccessToken
 	accessToken = resAccessToken.AccessToken
 	return
 	return
 }
 }
@@ -141,7 +141,7 @@ func (ak *StableAccessToken) GetAccessTokenContext(ctx context.Context) (accessT
 	}
 	}
 
 
 	expires := resAccessToken.ExpiresIn - 300
 	expires := resAccessToken.ExpiresIn - 300
-	_ = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
+	err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
 
 
 	accessToken = resAccessToken.AccessToken
 	accessToken = resAccessToken.AccessToken
 	return
 	return
@@ -219,9 +219,7 @@ func (ak *WorkAccessToken) GetAccessTokenContext(ctx context.Context) (accessTok
 
 
 	expires := resAccessToken.ExpiresIn - 1500
 	expires := resAccessToken.ExpiresIn - 1500
 	err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
 	err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
-	if err != nil {
-		return
-	}
+
 	accessToken = resAccessToken.AccessToken
 	accessToken = resAccessToken.AccessToken
 	return
 	return
 }
 }

+ 2 - 4
miniprogram/auth/auth.go

@@ -138,10 +138,8 @@ func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*Get
 	}
 	}
 
 
 	var result GetPhoneNumberResponse
 	var result GetPhoneNumberResponse
-	if err = util.DecodeWithError(response, &result, "phonenumber.getPhoneNumber"); err != nil {
-		return nil, err
-	}
-	return &result, nil
+	err = util.DecodeWithError(response, &result, "phonenumber.getPhoneNumber")
+	return &result, err
 }
 }
 
 
 // GetPhoneNumber 小程序通过code获取用户手机号
 // GetPhoneNumber 小程序通过code获取用户手机号

+ 1 - 6
miniprogram/business/phone_number.go

@@ -45,10 +45,5 @@ func (business *Business) GetPhoneNumber(in *GetPhoneNumberRequest) (info PhoneI
 		PhoneInfo PhoneInfo `json:"phone_info"`
 		PhoneInfo PhoneInfo `json:"phone_info"`
 	}
 	}
 	err = util.DecodeWithError(response, &resp, "business.GetPhoneNumber")
 	err = util.DecodeWithError(response, &resp, "business.GetPhoneNumber")
-	if nil != err {
-		return
-	}
-
-	info = resp.PhoneInfo
-	return
+	return resp.PhoneInfo, err
 }
 }

+ 2 - 2
miniprogram/message/message.go

@@ -398,7 +398,7 @@ type PushDataSecVodUpload struct {
 
 
 // SecVodUploadEvent 短剧媒资上传完成事件
 // SecVodUploadEvent 短剧媒资上传完成事件
 type SecVodUploadEvent struct {
 type SecVodUploadEvent struct {
-	MediaID       string `json:"media_id" xml:"media_id"`             // 媒资 id
+	MediaID       int64  `json:"media_id" xml:"media_id"`             // 媒资 id
 	SourceContext string `json:"source_context" xml:"source_context"` // 透传上传接口中开发者设置的值。
 	SourceContext string `json:"source_context" xml:"source_context"` // 透传上传接口中开发者设置的值。
 	ErrCode       int    `json:"errcode" xml:"errcode"`               // 错误码,上传失败时该值非
 	ErrCode       int    `json:"errcode" xml:"errcode"`               // 错误码,上传失败时该值非
 	ErrMsg        string `json:"errmsg" xml:"errmsg"`                 // 错误提示
 	ErrMsg        string `json:"errmsg" xml:"errmsg"`                 // 错误提示
@@ -412,7 +412,7 @@ type PushDataSecVodAudit struct {
 
 
 // SecVodAuditEvent 短剧媒资审核状态事件
 // SecVodAuditEvent 短剧媒资审核状态事件
 type SecVodAuditEvent struct {
 type SecVodAuditEvent struct {
-	DramaID       string           `json:"drama_id" xml:"drama_id"`             // 剧目 id
+	DramaID       int64            `json:"drama_id" xml:"drama_id"`             // 剧目 id
 	SourceContext string           `json:"source_context" xml:"source_context"` // 透传上传接口中开发者设置的值
 	SourceContext string           `json:"source_context" xml:"source_context"` // 透传上传接口中开发者设置的值
 	AuditDetail   DramaAuditDetail `json:"audit_detail" xml:"audit_detail"`     // 剧目审核结果,单独每一集的审核结果可以根据 drama_id 查询剧集详情得到
 	AuditDetail   DramaAuditDetail `json:"audit_detail" xml:"audit_detail"`     // 剧目审核结果,单独每一集的审核结果可以根据 drama_id 查询剧集详情得到
 }
 }

+ 102 - 0
miniprogram/message/updatable_msg.go

@@ -0,0 +1,102 @@
+package message
+
+import (
+	"fmt"
+
+	"github.com/silenceper/wechat/v2/miniprogram/context"
+	"github.com/silenceper/wechat/v2/util"
+)
+
+const (
+	// createActivityURL 创建activity_id
+	createActivityURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/activityid/create?access_token=%s"
+	// SendUpdatableMsgURL 修改动态消息
+	setUpdatableMsgURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/updatablemsg/send?access_token=%s"
+)
+
+// UpdatableTargetState 动态消息状态
+type UpdatableTargetState int
+
+const (
+	// TargetStateNotStarted 未开始
+	TargetStateNotStarted UpdatableTargetState = 0
+	// TargetStateStarted 已开始
+	TargetStateStarted UpdatableTargetState = 1
+	// TargetStateFinished 已结束
+	TargetStateFinished UpdatableTargetState = 2
+)
+
+// UpdatableMessage 动态消息
+type UpdatableMessage struct {
+	*context.Context
+}
+
+// NewUpdatableMessage 实例化
+func NewUpdatableMessage(ctx *context.Context) *UpdatableMessage {
+	return &UpdatableMessage{
+		Context: ctx,
+	}
+}
+
+// CreateActivityID 创建activity_id
+func (updatableMessage *UpdatableMessage) CreateActivityID() (res CreateActivityIDResponse, err error) {
+	accessToken, err := updatableMessage.GetAccessToken()
+	if err != nil {
+		return
+	}
+
+	uri := fmt.Sprintf(createActivityURL, accessToken)
+	response, err := util.HTTPGet(uri)
+	if err != nil {
+		return
+	}
+	err = util.DecodeWithError(response, &res, "CreateActivityID")
+	return
+}
+
+// SetUpdatableMsg 修改动态消息
+func (updatableMessage *UpdatableMessage) SetUpdatableMsg(activityID string, targetState UpdatableTargetState, template UpdatableMsgTemplate) (err error) {
+	accessToken, err := updatableMessage.GetAccessToken()
+	if err != nil {
+		return
+	}
+
+	uri := fmt.Sprintf(setUpdatableMsgURL, accessToken)
+	data := SendUpdatableMsgReq{
+		ActivityID:   activityID,
+		TargetState:  targetState,
+		TemplateInfo: template,
+	}
+
+	response, err := util.PostJSON(uri, data)
+	if err != nil {
+		return
+	}
+	return util.DecodeWithCommonError(response, "SendUpdatableMsg")
+}
+
+// CreateActivityIDResponse 创建activity_id 返回
+type CreateActivityIDResponse struct {
+	util.CommonError
+
+	ActivityID     string `json:"activity_id"`
+	ExpirationTime int64  `json:"expiration_time"`
+}
+
+// UpdatableMsgTemplate 动态消息模板
+type UpdatableMsgTemplate struct {
+	ParameterList []UpdatableMsgParameter `json:"parameter_list"`
+}
+
+// UpdatableMsgParameter 动态消息参数
+type UpdatableMsgParameter struct {
+	Name  string `json:"name"`
+	Value string `json:"value"`
+}
+
+// SendUpdatableMsgReq 修改动态消息参数
+type SendUpdatableMsgReq struct {
+	ActivityID   string               `json:"activity_id"`
+	TemplateInfo UpdatableMsgTemplate `json:"template_info"`
+	TargetState  UpdatableTargetState `json:"target_state"`
+}

+ 11 - 0
miniprogram/miniprogram.go

@@ -15,6 +15,7 @@ import (
 	"github.com/silenceper/wechat/v2/miniprogram/order"
 	"github.com/silenceper/wechat/v2/miniprogram/order"
 	"github.com/silenceper/wechat/v2/miniprogram/privacy"
 	"github.com/silenceper/wechat/v2/miniprogram/privacy"
 	"github.com/silenceper/wechat/v2/miniprogram/qrcode"
 	"github.com/silenceper/wechat/v2/miniprogram/qrcode"
+	"github.com/silenceper/wechat/v2/miniprogram/redpacketcover"
 	"github.com/silenceper/wechat/v2/miniprogram/riskcontrol"
 	"github.com/silenceper/wechat/v2/miniprogram/riskcontrol"
 	"github.com/silenceper/wechat/v2/miniprogram/security"
 	"github.com/silenceper/wechat/v2/miniprogram/security"
 	"github.com/silenceper/wechat/v2/miniprogram/shortlink"
 	"github.com/silenceper/wechat/v2/miniprogram/shortlink"
@@ -155,3 +156,13 @@ func (miniProgram *MiniProgram) GetShipping() *order.Shipping {
 func (miniProgram *MiniProgram) GetMiniDrama() *minidrama.MiniDrama {
 func (miniProgram *MiniProgram) GetMiniDrama() *minidrama.MiniDrama {
 	return minidrama.NewMiniDrama(miniProgram.ctx)
 	return minidrama.NewMiniDrama(miniProgram.ctx)
 }
 }
+
+// GetRedPacketCover 小程序微信红包封面 API
+func (miniProgram *MiniProgram) GetRedPacketCover() *redpacketcover.RedPacketCover {
+	return redpacketcover.NewRedPacketCover(miniProgram.ctx)
+}
+
+// GetUpdatableMessage 小程序动态消息
+func (miniProgram *MiniProgram) GetUpdatableMessage() *message.UpdatableMessage {
+	return message.NewUpdatableMessage(miniProgram.ctx)
+}

+ 4 - 14
miniprogram/privacy/privacy.go

@@ -103,11 +103,8 @@ func (s *Privacy) GetPrivacySetting(privacyVer int) (GetPrivacySettingResponse,
 	}
 	}
 	// 返回错误信息
 	// 返回错误信息
 	var result GetPrivacySettingResponse
 	var result GetPrivacySettingResponse
-	if err = util.DecodeWithError(response, &result, "getprivacysetting"); err != nil {
-		return GetPrivacySettingResponse{}, err
-	}
-
-	return result, nil
+	err = util.DecodeWithError(response, &result, "getprivacysetting")
+	return result, err
 }
 }
 
 
 // SetPrivacySetting 更新小程序权限配置
 // SetPrivacySetting 更新小程序权限配置
@@ -130,11 +127,7 @@ func (s *Privacy) SetPrivacySetting(privacyVer int, ownerSetting OwnerSetting, s
 	}
 	}
 
 
 	// 返回错误信息
 	// 返回错误信息
-	if err = util.DecodeWithCommonError(response, "setprivacysetting"); err != nil {
-		return err
-	}
-
-	return err
+	return util.DecodeWithCommonError(response, "setprivacysetting")
 }
 }
 
 
 // UploadPrivacyExtFileResponse 上传权限定义模板响应参数
 // UploadPrivacyExtFileResponse 上传权限定义模板响应参数
@@ -159,9 +152,6 @@ func (s *Privacy) UploadPrivacyExtFile(fileData []byte) (UploadPrivacyExtFileRes
 
 
 	// 返回错误信息
 	// 返回错误信息
 	var result UploadPrivacyExtFileResponse
 	var result UploadPrivacyExtFileResponse
-	if err = util.DecodeWithError(response, &result, "setprivacysetting"); err != nil {
-		return UploadPrivacyExtFileResponse{}, err
-	}
-
+	err = util.DecodeWithError(response, &result, "setprivacysetting")
 	return result, err
 	return result, err
 }
 }

+ 59 - 0
miniprogram/redpacketcover/redpacketcover.go

@@ -0,0 +1,59 @@
+package redpacketcover
+
+import (
+	"fmt"
+
+	"github.com/silenceper/wechat/v2/miniprogram/context"
+	"github.com/silenceper/wechat/v2/util"
+)
+
+const (
+	getRedPacketCoverURL = "https://api.weixin.qq.com/redpacketcover/wxapp/cover_url/get_by_token?access_token=%s"
+)
+
+// RedPacketCover struct
+type RedPacketCover struct {
+	*context.Context
+}
+
+// NewRedPacketCover 实例
+func NewRedPacketCover(context *context.Context) *RedPacketCover {
+	redPacketCover := new(RedPacketCover)
+	redPacketCover.Context = context
+	return redPacketCover
+}
+
+// GetRedPacketCoverRequest 获取微信红包封面参数
+type GetRedPacketCoverRequest struct {
+	// openid 可领取用户的openid
+	OpenID string `json:"openid"`
+	// ctoken 在红包封面平台获取发放ctoken(需要指定可以发放的appid)
+	CToken string `json:"ctoken"`
+}
+
+// GetRedPacketCoverResp 获取微信红包封面
+type GetRedPacketCoverResp struct {
+	util.CommonError
+	Data struct {
+		URL string `json:"url"`
+	} `json:"data"` // 唯一请求标识
+}
+
+// GetRedPacketCoverURL 获得指定用户可以领取的红包封面链接。获取参数ctoken参考微信红包封面开放平台
+// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/red-packet-cover/getRedPacketCoverUrl.html
+func (cover *RedPacketCover) GetRedPacketCoverURL(coderParams GetRedPacketCoverRequest) (res GetRedPacketCoverResp, err error) {
+	accessToken, err := cover.GetAccessToken()
+	if err != nil {
+		return
+	}
+
+	uri := fmt.Sprintf(getRedPacketCoverURL, accessToken)
+	response, err := util.PostJSON(uri, coderParams)
+	if err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	err = util.DecodeWithError(response, &res, "GetRedPacketCoverURL")
+	return
+}

+ 2 - 12
miniprogram/security/security.go

@@ -51,12 +51,7 @@ func (security *Security) MediaCheckAsyncV1(in *MediaCheckAsyncV1Request) (trace
 		TraceID string `json:"trace_id"`
 		TraceID string `json:"trace_id"`
 	}
 	}
 	err = util.DecodeWithError(response, &res, "MediaCheckAsyncV1")
 	err = util.DecodeWithError(response, &res, "MediaCheckAsyncV1")
-	if err != nil {
-		return
-	}
-
-	traceID = res.TraceID
-	return
+	return res.TraceID, err
 }
 }
 
 
 // MediaCheckAsyncRequest 图片/音频异步校验请求参数
 // MediaCheckAsyncRequest 图片/音频异步校验请求参数
@@ -93,12 +88,7 @@ func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID s
 		TraceID string `json:"trace_id"`
 		TraceID string `json:"trace_id"`
 	}
 	}
 	err = util.DecodeWithError(response, &res, "MediaCheckAsync")
 	err = util.DecodeWithError(response, &res, "MediaCheckAsync")
-	if err != nil {
-		return
-	}
-
-	traceID = res.TraceID
-	return
+	return res.TraceID, err
 }
 }
 
 
 // ImageCheckV1 校验一张图片是否含有违法违规内容(同步)
 // ImageCheckV1 校验一张图片是否含有违法违规内容(同步)

+ 1 - 5
miniprogram/shortlink/shortlink.go

@@ -60,11 +60,7 @@ func (shortLink *ShortLink) generate(shortLinkParams ShortLinker) (string, error
 	// 使用通用方法返回错误
 	// 使用通用方法返回错误
 	var res resShortLinker
 	var res resShortLinker
 	err = util.DecodeWithError(response, &res, "GenerateShortLink")
 	err = util.DecodeWithError(response, &res, "GenerateShortLink")
-	if err != nil {
-		return "", err
-	}
-
-	return res.Link, nil
+	return res.Link, err
 }
 }
 
 
 // GenerateShortLinkPermanent 生成永久 shortLink
 // GenerateShortLinkPermanent 生成永久 shortLink

+ 1 - 5
miniprogram/subscribe/subscribe.go

@@ -168,11 +168,7 @@ func (s *Subscribe) Add(ShortID string, kidList []int, sceneDesc string) (templa
 	}
 	}
 	var result resSubscribeAdd
 	var result resSubscribeAdd
 	err = util.DecodeWithError(response, &result, "AddSubscribe")
 	err = util.DecodeWithError(response, &result, "AddSubscribe")
-	if err != nil {
-		return
-	}
-	templateID = result.TemplateID
-	return
+	return result.TemplateID, err
 }
 }
 
 
 // Delete 删除私有模板
 // Delete 删除私有模板

+ 1 - 4
miniprogram/urllink/urllink.go

@@ -65,8 +65,5 @@ func (u *URLLink) Generate(params *ULParams) (string, error) {
 	}
 	}
 	var resp ULResult
 	var resp ULResult
 	err = util.DecodeWithError(response, &resp, "URLLink.Generate")
 	err = util.DecodeWithError(response, &resp, "URLLink.Generate")
-	if err != nil {
-		return "", err
-	}
-	return resp.URLLink, nil
+	return resp.URLLink, err
 }
 }

+ 2 - 6
miniprogram/urlscheme/query.go

@@ -37,7 +37,7 @@ type SchemeInfo struct {
 // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
 // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
 type resQueryScheme struct {
 type resQueryScheme struct {
 	// 通用错误
 	// 通用错误
-	*util.CommonError
+	util.CommonError
 	// scheme 配置
 	// scheme 配置
 	SchemeInfo SchemeInfo `json:"scheme_info"`
 	SchemeInfo SchemeInfo `json:"scheme_info"`
 	// 访问该链接的openid,没有用户访问过则为空字符串
 	// 访问该链接的openid,没有用户访问过则为空字符串
@@ -62,9 +62,5 @@ func (u *URLScheme) QueryScheme(querySchemeParams QueryScheme) (schemeInfo Schem
 	// 使用通用方法返回错误
 	// 使用通用方法返回错误
 	var res resQueryScheme
 	var res resQueryScheme
 	err = util.DecodeWithError(response, &res, "QueryScheme")
 	err = util.DecodeWithError(response, &res, "QueryScheme")
-	if err != nil {
-		return
-	}
-
-	return res.SchemeInfo, res.VisitOpenid, nil
+	return res.SchemeInfo, res.VisitOpenid, err
 }
 }

+ 1 - 4
miniprogram/urlscheme/urlscheme.go

@@ -78,8 +78,5 @@ func (u *URLScheme) Generate(params *USParams) (string, error) {
 	}
 	}
 	var resp USResult
 	var resp USResult
 	err = util.DecodeWithError(response, &resp, "URLScheme.Generate")
 	err = util.DecodeWithError(response, &resp, "URLScheme.Generate")
-	if err != nil {
-		return "", err
-	}
-	return resp.OpenLink, nil
+	return resp.OpenLink, err
 }
 }

+ 2 - 5
officialaccount/basic/short_url.go

@@ -44,9 +44,6 @@ func (basic *Basic) Long2ShortURL(longURL string) (shortURL string, err error) {
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
-	if err = util.DecodeWithError(responseBytes, resp, long2shortAction); err != nil {
-		return
-	}
-	shortURL = resp.ShortURL
-	return
+	err = util.DecodeWithError(responseBytes, resp, long2shortAction)
+	return resp.ShortURL, err
 }
 }

+ 12 - 2
officialaccount/broadcast/broadcast.go

@@ -79,6 +79,10 @@ type sendRequest struct {
 	Mpnews map[string]interface{} `json:"mpnews,omitempty"`
 	Mpnews map[string]interface{} `json:"mpnews,omitempty"`
 	// 发送语音
 	// 发送语音
 	Voice map[string]interface{} `json:"voice,omitempty"`
 	Voice map[string]interface{} `json:"voice,omitempty"`
+	// 发送视频
+	Mpvideo map[string]interface{} `json:"mpvideo,omitempty"`
+	// 发送图片-预览使用
+	Image map[string]interface{} `json:"image,omitempty"`
 	// 发送图片
 	// 发送图片
 	Images *Image `json:"images,omitempty"`
 	Images *Image `json:"images,omitempty"`
 	// 发送卡券
 	// 发送卡券
@@ -183,7 +187,13 @@ func (broadcast *Broadcast) SendImage(user *User, images *Image) (*Result, error
 		ToUser:  nil,
 		ToUser:  nil,
 		MsgType: MsgTypeImage,
 		MsgType: MsgTypeImage,
 	}
 	}
-	req.Images = images
+	if broadcast.preview {
+		req.Image = map[string]interface{}{
+			"media_id": images.MediaIDs[0],
+		}
+	} else {
+		req.Images = images
+	}
 	req, sendURL := broadcast.chooseTagOrOpenID(user, req)
 	req, sendURL := broadcast.chooseTagOrOpenID(user, req)
 	url := fmt.Sprintf("%s?access_token=%s", sendURL, ak)
 	url := fmt.Sprintf("%s?access_token=%s", sendURL, ak)
 	data, err := util.PostJSON(url, req)
 	data, err := util.PostJSON(url, req)
@@ -205,7 +215,7 @@ func (broadcast *Broadcast) SendVideo(user *User, mediaID string, title, descrip
 		ToUser:  nil,
 		ToUser:  nil,
 		MsgType: MsgTypeVideo,
 		MsgType: MsgTypeVideo,
 	}
 	}
-	req.Voice = map[string]interface{}{
+	req.Mpvideo = map[string]interface{}{
 		"media_id":    mediaID,
 		"media_id":    mediaID,
 		"title":       title,
 		"title":       title,
 		"description": description,
 		"description": description,

+ 2 - 10
officialaccount/customerservice/manager.go

@@ -72,11 +72,7 @@ func (csm *Manager) List() (customerServiceList []*KeFuInfo, err error) {
 	}
 	}
 	var res resKeFuList
 	var res resKeFuList
 	err = util.DecodeWithError(response, &res, "ListCustomerService")
 	err = util.DecodeWithError(response, &res, "ListCustomerService")
-	if err != nil {
-		return
-	}
-	customerServiceList = res.KfList
-	return
+	return res.KfList, err
 }
 }
 
 
 // KeFuOnlineInfo 客服在线信息
 // KeFuOnlineInfo 客服在线信息
@@ -107,11 +103,7 @@ func (csm *Manager) OnlineList() (customerServiceOnlineList []*KeFuOnlineInfo, e
 	}
 	}
 	var res resKeFuOnlineList
 	var res resKeFuOnlineList
 	err = util.DecodeWithError(response, &res, "ListOnlineCustomerService")
 	err = util.DecodeWithError(response, &res, "ListOnlineCustomerService")
-	if err != nil {
-		return
-	}
-	customerServiceOnlineList = res.KfOnlineList
-	return
+	return res.KfOnlineList, err
 }
 }
 
 
 // Add 添加客服账号
 // Add 添加客服账号

+ 0 - 3
officialaccount/datacube/publisher.go

@@ -183,9 +183,6 @@ func (cube *DataCube) fetchData(params ParamsPublisher) (response []byte, err er
 	uri := fmt.Sprintf("%s?%s", publisherURL, v.Encode())
 	uri := fmt.Sprintf("%s?%s", publisherURL, v.Encode())
 
 
 	response, err = util.HTTPGet(uri)
 	response, err = util.HTTPGet(uri)
-	if err != nil {
-		return
-	}
 	return
 	return
 }
 }
 
 

+ 3 - 17
officialaccount/draft/draft.go

@@ -64,11 +64,7 @@ func (draft *Draft) AddDraft(articles []*Article) (mediaID string, err error) {
 		MediaID string `json:"media_id"`
 		MediaID string `json:"media_id"`
 	}
 	}
 	err = util.DecodeWithError(response, &res, "AddDraft")
 	err = util.DecodeWithError(response, &res, "AddDraft")
-	if err != nil {
-		return
-	}
-	mediaID = res.MediaID
-	return
+	return res.MediaID, err
 }
 }
 
 
 // GetDraft 获取草稿
 // GetDraft 获取草稿
@@ -94,12 +90,7 @@ func (draft *Draft) GetDraft(mediaID string) (articles []*Article, err error) {
 		NewsItem []*Article `json:"news_item"`
 		NewsItem []*Article `json:"news_item"`
 	}
 	}
 	err = util.DecodeWithError(response, &res, "GetDraft")
 	err = util.DecodeWithError(response, &res, "GetDraft")
-	if err != nil {
-		return
-	}
-
-	articles = res.NewsItem
-	return
+	return res.NewsItem, err
 }
 }
 
 
 // DeleteDraft 删除草稿
 // DeleteDraft 删除草稿
@@ -172,12 +163,7 @@ func (draft *Draft) CountDraft() (total uint, err error) {
 		Total uint `json:"total_count"`
 		Total uint `json:"total_count"`
 	}
 	}
 	err = util.DecodeWithError(response, &res, "CountDraft")
 	err = util.DecodeWithError(response, &res, "CountDraft")
-	if nil != err {
-		return
-	}
-
-	total = res.Total
-	return
+	return res.Total, err
 }
 }
 
 
 // ArticleList 草稿列表
 // ArticleList 草稿列表

+ 2 - 12
officialaccount/freepublish/freepublish.go

@@ -73,12 +73,7 @@ func (freePublish *FreePublish) Publish(mediaID string) (publishID int64, err er
 		PublishID int64 `json:"publish_id"`
 		PublishID int64 `json:"publish_id"`
 	}
 	}
 	err = util.DecodeWithError(response, &res, "SubmitFreePublish")
 	err = util.DecodeWithError(response, &res, "SubmitFreePublish")
-	if err != nil {
-		return
-	}
-
-	publishID = res.PublishID
-	return
+	return res.PublishID, err
 }
 }
 
 
 // PublishStatusList 发布任务状态列表
 // PublishStatusList 发布任务状态列表
@@ -191,12 +186,7 @@ func (freePublish *FreePublish) First(articleID string) (list []Article, err err
 		NewsItem []Article `json:"news_item"`
 		NewsItem []Article `json:"news_item"`
 	}
 	}
 	err = util.DecodeWithError(response, &res, "FirstFreePublish")
 	err = util.DecodeWithError(response, &res, "FirstFreePublish")
-	if err != nil {
-		return
-	}
-
-	list = res.NewsItem
-	return
+	return res.NewsItem, err
 }
 }
 
 
 // ArticleList 发布列表
 // ArticleList 发布列表

+ 5 - 26
officialaccount/message/subscribe.go

@@ -90,11 +90,7 @@ func (tpl *Subscribe) List() (templateList []*PrivateSubscribeItem, err error) {
 	}
 	}
 	var res resPrivateSubscribeList
 	var res resPrivateSubscribeList
 	err = util.DecodeWithError(response, &res, "ListSubscribe")
 	err = util.DecodeWithError(response, &res, "ListSubscribe")
-	if err != nil {
-		return
-	}
-	templateList = res.SubscriptionList
-	return
+	return res.SubscriptionList, err
 }
 }
 
 
 type resSubscribeAdd struct {
 type resSubscribeAdd struct {
@@ -123,11 +119,7 @@ func (tpl *Subscribe) Add(ShortID string, kidList []int, sceneDesc string) (temp
 	}
 	}
 	var result resSubscribeAdd
 	var result resSubscribeAdd
 	err = util.DecodeWithError(response, &result, "AddSubscribe")
 	err = util.DecodeWithError(response, &result, "AddSubscribe")
-	if err != nil {
-		return
-	}
-	templateID = result.TemplateID
-	return
+	return result.TemplateID, err
 }
 }
 
 
 // Delete 删除私有模板
 // Delete 删除私有模板
@@ -175,11 +167,7 @@ func (tpl *Subscribe) GetCategory() (categoryList []*PublicTemplateCategory, err
 	}
 	}
 	var result resSubscribeCategoryList
 	var result resSubscribeCategoryList
 	err = util.DecodeWithError(response, &result, "GetCategory")
 	err = util.DecodeWithError(response, &result, "GetCategory")
-	if err != nil {
-		return
-	}
-	categoryList = result.CategoryList
-	return
+	return result.CategoryList, err
 }
 }
 
 
 // PublicTemplateKeyWords 模板中的关键词
 // PublicTemplateKeyWords 模板中的关键词
@@ -210,11 +198,7 @@ func (tpl *Subscribe) GetPubTplKeyWordsByID(titleID string) (keyWordsList []*Pub
 	}
 	}
 	var result resPublicTemplateKeyWordsList
 	var result resPublicTemplateKeyWordsList
 	err = util.DecodeWithError(response, &result, "GetPublicTemplateKeyWords")
 	err = util.DecodeWithError(response, &result, "GetPublicTemplateKeyWords")
-	if err != nil {
-		return
-	}
-	keyWordsList = result.KeyWordsList
-	return
+	return result.KeyWordsList, err
 }
 }
 
 
 // PublicTemplateTitle 类目下的公共模板
 // PublicTemplateTitle 类目下的公共模板
@@ -246,10 +230,5 @@ func (tpl *Subscribe) GetPublicTemplateTitleList(ids string, start int, limit in
 	}
 	}
 	var result resPublicTemplateTitleList
 	var result resPublicTemplateTitleList
 	err = util.DecodeWithError(response, &result, "GetPublicTemplateTitle")
 	err = util.DecodeWithError(response, &result, "GetPublicTemplateTitle")
-	if err != nil {
-		return
-	}
-	count = result.Count
-	templateTitleList = result.TemplateTitleList
-	return
+	return result.Count, result.TemplateTitleList, err
 }
 }

+ 2 - 10
officialaccount/message/template.go

@@ -111,11 +111,7 @@ func (tpl *Template) List() (templateList []*TemplateItem, err error) {
 	}
 	}
 	var res resTemplateList
 	var res resTemplateList
 	err = util.DecodeWithError(response, &res, "ListTemplate")
 	err = util.DecodeWithError(response, &res, "ListTemplate")
-	if err != nil {
-		return
-	}
-	templateList = res.TemplateList
-	return
+	return res.TemplateList, err
 }
 }
 
 
 type resTemplateAdd struct {
 type resTemplateAdd struct {
@@ -143,11 +139,7 @@ func (tpl *Template) Add(shortID string) (templateID string, err error) {
 
 
 	var result resTemplateAdd
 	var result resTemplateAdd
 	err = util.DecodeWithError(response, &result, "AddTemplate")
 	err = util.DecodeWithError(response, &result, "AddTemplate")
-	if err != nil {
-		return
-	}
-	templateID = result.TemplateID
-	return
+	return result.TemplateID, err
 }
 }
 
 
 // Delete 删除私有模板.
 // Delete 删除私有模板.

+ 0 - 4
officialaccount/user/migrate.go

@@ -62,10 +62,6 @@ func (user *User) ListChangeOpenIDs(fromAppID string, openIDs ...string) (list *
 	}
 	}
 
 
 	err = util.DecodeWithError(resp, list, "ListChangeOpenIDs")
 	err = util.DecodeWithError(resp, list, "ListChangeOpenIDs")
-	if err != nil {
-		return
-	}
-
 	return
 	return
 }
 }
 
 

+ 1 - 7
officialaccount/user/tag.go

@@ -126,10 +126,7 @@ func (user *User) GetTag() (tags []*TagInfo, err error) {
 		Tags []*TagInfo `json:"tags"`
 		Tags []*TagInfo `json:"tags"`
 	}
 	}
 	err = json.Unmarshal(response, &result)
 	err = json.Unmarshal(response, &result)
-	if err != nil {
-		return
-	}
-	return result.Tags, nil
+	return result.Tags, err
 }
 }
 
 
 // OpenIDListByTag 获取标签下粉丝列表
 // OpenIDListByTag 获取标签下粉丝列表
@@ -154,9 +151,6 @@ func (user *User) OpenIDListByTag(tagID int32, nextOpenID ...string) (userList *
 	}
 	}
 	userList = new(TagOpenIDList)
 	userList = new(TagOpenIDList)
 	err = json.Unmarshal(response, &userList)
 	err = json.Unmarshal(response, &userList)
-	if err != nil {
-		return
-	}
 	return
 	return
 }
 }
 
 

+ 2 - 5
openplatform/context/accessToken.go

@@ -100,11 +100,8 @@ func (ctx *Context) GetPreCodeContext(stdCtx context.Context) (string, error) {
 	var ret struct {
 	var ret struct {
 		PreCode string `json:"pre_auth_code"`
 		PreCode string `json:"pre_auth_code"`
 	}
 	}
-	if err := json.Unmarshal(body, &ret); err != nil {
-		return "", err
-	}
-
-	return ret.PreCode, nil
+	err = json.Unmarshal(body, &ret)
+	return ret.PreCode, err
 }
 }
 
 
 // GetPreCode 获取预授权码
 // GetPreCode 获取预授权码

+ 184 - 0
openplatform/miniprogram/basic/basic.go

@@ -9,6 +9,12 @@ import (
 
 
 const (
 const (
 	getAccountBasicInfoURL = "https://api.weixin.qq.com/cgi-bin/account/getaccountbasicinfo"
 	getAccountBasicInfoURL = "https://api.weixin.qq.com/cgi-bin/account/getaccountbasicinfo"
+	checkNickNameURL       = "https://api.weixin.qq.com/cgi-bin/wxverify/checkwxverifynickname"
+	setNickNameURL         = "https://api.weixin.qq.com/wxa/setnickname"
+	setSignatureURL        = "https://api.weixin.qq.com/cgi-bin/account/modifysignature"
+	setHeadImageURL        = "https://api.weixin.qq.com/cgi-bin/account/modifyheadimage"
+	getSearchStatusURL     = "https://api.weixin.qq.com/wxa/getwxasearchstatus"
+	setSearchStatusURL     = "https://api.weixin.qq.com/wxa/changewxasearchstatus"
 )
 )
 
 
 // Basic 基础信息设置
 // Basic 基础信息设置
@@ -51,3 +57,181 @@ func (basic *Basic) GetAccountBasicInfo() (*AccountBasicInfo, error) {
 // TODO
 // TODO
 // func (encryptor *Basic) modifyDomain() {
 // func (encryptor *Basic) modifyDomain() {
 // }
 // }
+
+// CheckNickNameResp 小程序名称检测结果
+type CheckNickNameResp struct {
+	util.CommonError
+	HitCondition bool   `json:"hit_condition"` // 是否命中关键字策略。若命中,可以选填关键字材料
+	Wording      string `json:"wording"`       // 命中关键字的说明描述
+}
+
+// CheckNickName 检测微信认证的名称是否符合规则
+// ref: https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/basic-info-management/checkNickName.html
+func (basic *Basic) CheckNickName(nickname string) (*CheckNickNameResp, error) {
+	ak, err := basic.GetAuthrAccessToken(basic.AppID)
+	if err != nil {
+		return nil, err
+	}
+	url := fmt.Sprintf("%s?access_token=%s", checkNickNameURL, ak)
+	data, err := util.PostJSON(url, map[string]string{
+		"nick_name": nickname,
+	})
+	if err != nil {
+		return nil, err
+	}
+	res := &CheckNickNameResp{}
+	err = util.DecodeWithError(data, res, "CheckNickName")
+	return res, err
+}
+
+// SetNickNameResp 设置小程序名称结果
+type SetNickNameResp struct {
+	util.CommonError
+	AuditID int64  `json:"audit_id"` // 审核单Id,通过用于查询改名审核状态
+	Wording string `json:"wording"`  // 材料说明
+}
+
+// SetNickNameParam 设置小程序名称参数
+type SetNickNameParam struct {
+	NickName           string `json:"nick_name"`                      // 昵称,不支持包含“小程序”关键字的昵称
+	IDCard             string `json:"id_card,omitempty"`              // 身份证照片 mediaid,个人号必填
+	License            string `json:"license,omitempty"`              // 组织机构代码证或营业执照 mediaid,组织号必填
+	NameingOtherStuff1 string `json:"naming_other_stuff_1,omitempty"` // 其他证明材料 mediaid,选填
+	NameingOtherStuff2 string `json:"naming_other_stuff_2,omitempty"` // 其他证明材料 mediaid,选填
+	NameingOtherStuff3 string `json:"naming_other_stuff_3,omitempty"` // 其他证明材料 mediaid,选填
+	NameingOtherStuff4 string `json:"naming_other_stuff_4,omitempty"` // 其他证明材料 mediaid,选填
+	NameingOtherStuff5 string `json:"naming_other_stuff_5,omitempty"` // 其他证明材料 mediaid,选填
+}
+
+// SetNickName 设置小程序名称
+func (basic *Basic) SetNickName(nickname string) (*SetNickNameResp, error) {
+	return basic.SetNickNameFull(&SetNickNameParam{
+		NickName: nickname,
+	})
+}
+
+// SetNickNameFull 设置小程序名称
+// ref: https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/basic-info-management/setNickName.html
+func (basic *Basic) SetNickNameFull(param *SetNickNameParam) (*SetNickNameResp, error) {
+	ak, err := basic.GetAuthrAccessToken(basic.AppID)
+	if err != nil {
+		return nil, err
+	}
+	url := fmt.Sprintf("%s?access_token=%s", setNickNameURL, ak)
+	data, err := util.PostJSON(url, param)
+	if err != nil {
+		return nil, err
+	}
+	res := &SetNickNameResp{}
+	err = util.DecodeWithError(data, res, "SetNickName")
+	return res, err
+}
+
+// SetSignatureResp 小程序功能介绍修改结果
+type SetSignatureResp struct {
+	util.CommonError
+}
+
+// SetSignature 小程序修改功能介绍
+// ref: https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/basic-info-management/setSignature.html
+func (basic *Basic) SetSignature(signature string) error {
+	ak, err := basic.GetAuthrAccessToken(basic.AppID)
+	if err != nil {
+		return err
+	}
+	url := fmt.Sprintf("%s?access_token=%s", setSignatureURL, ak)
+	data, err := util.PostJSON(url, map[string]string{
+		"signature": signature,
+	})
+	if err != nil {
+		return err
+	}
+	return util.DecodeWithError(data, &SetSignatureResp{}, "SetSignature")
+}
+
+// GetSearchStatusResp 查询小程序当前是否可被搜索
+type GetSearchStatusResp struct {
+	util.CommonError
+	Status int `json:"status"` // 1 表示不可搜索,0 表示可搜索
+}
+
+// GetSearchStatus 查询小程序当前是否可被搜索
+// ref: https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/basic-info-management/getSearchStatus.html
+func (basic *Basic) GetSearchStatus(signature string) (*GetSearchStatusResp, error) {
+	ak, err := basic.GetAuthrAccessToken(basic.AppID)
+	if err != nil {
+		return nil, err
+	}
+	url := fmt.Sprintf("%s?access_token=%s", getSearchStatusURL, ak)
+	data, err := util.HTTPGet(url)
+	if err != nil {
+		return nil, err
+	}
+	res := &GetSearchStatusResp{}
+	err = util.DecodeWithError(data, res, "GetSearchStatus")
+	return res, err
+}
+
+// SetSearchStatusResp 小程序是否可被搜索修改结果
+type SetSearchStatusResp struct {
+	util.CommonError
+}
+
+// SetSearchStatus 修改小程序是否可被搜索
+// status: 1 表示不可搜索,0 表示可搜索
+// ref: https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/basic-info-management/setSearchStatus.html
+func (basic *Basic) SetSearchStatus(status int) error {
+	ak, err := basic.GetAuthrAccessToken(basic.AppID)
+	if err != nil {
+		return err
+	}
+	url := fmt.Sprintf("%s?access_token=%s", setSearchStatusURL, ak)
+	data, err := util.PostJSON(url, map[string]int{
+		"status": status,
+	})
+	if err != nil {
+		return err
+	}
+	return util.DecodeWithError(data, &SetSearchStatusResp{}, "SetSearchStatus")
+}
+
+// SetHeadImageResp 小程序头像修改结果
+type SetHeadImageResp struct {
+	util.CommonError
+}
+
+// SetHeadImageParam 小程序头像修改参数
+type SetHeadImageParam struct {
+	HeadImageMediaID string `json:"head_img_media_id"` // 头像素材 media_id
+	X1               string `json:"x1"`                // 裁剪框左上角 x 坐标(取值范围:[0, 1])
+	Y1               string `json:"y1"`                // 裁剪框左上角 y 坐标(取值范围:[0, 1])
+	X2               string `json:"x2"`                // 裁剪框右下角 x 坐标(取值范围:[0, 1])
+	Y2               string `json:"y2"`                // 裁剪框右下角 y 坐标(取值范围:[0, 1])
+}
+
+// SetHeadImage 修改小程序头像
+func (basic *Basic) SetHeadImage(imgMediaID string) error {
+	return basic.SetHeadImageFull(&SetHeadImageParam{
+		HeadImageMediaID: imgMediaID,
+		X1:               "0",
+		Y1:               "0",
+		X2:               "1",
+		Y2:               "1",
+	})
+}
+
+// SetHeadImageFull 修改小程序头像
+// 新增临时素材: https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/New_temporary_materials.html
+// ref: https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/basic-info-management/setHeadImage.html
+func (basic *Basic) SetHeadImageFull(param *SetHeadImageParam) error {
+	ak, err := basic.GetAuthrAccessToken(basic.AppID)
+	if err != nil {
+		return err
+	}
+	url := fmt.Sprintf("%s?access_token=%s", setHeadImageURL, ak)
+	data, err := util.PostJSON(url, param)
+	if err != nil {
+		return err
+	}
+	return util.DecodeWithError(data, &SetHeadImageResp{}, "account/setheadimage")
+}

+ 13 - 11
util/http.go

@@ -22,6 +22,9 @@ type URIModifier func(uri string) string
 
 
 var uriModifier URIModifier
 var uriModifier URIModifier
 
 
+// DefaultHTTPClient 默认httpClient
+var DefaultHTTPClient = http.DefaultClient
+
 // SetURIModifier 设置URI修改器
 // SetURIModifier 设置URI修改器
 func SetURIModifier(fn URIModifier) {
 func SetURIModifier(fn URIModifier) {
 	uriModifier = fn
 	uriModifier = fn
@@ -41,7 +44,7 @@ func HTTPGetContext(ctx context.Context, uri string) ([]byte, error) {
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	response, err := http.DefaultClient.Do(request)
+	response, err := DefaultHTTPClient.Do(request)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -73,7 +76,7 @@ func HTTPPostContext(ctx context.Context, uri string, data []byte, header map[st
 		request.Header.Set(key, value)
 		request.Header.Set(key, value)
 	}
 	}
 
 
-	response, err := http.DefaultClient.Do(request)
+	response, err := DefaultHTTPClient.Do(request)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -102,7 +105,7 @@ func PostJSONContext(ctx context.Context, uri string, obj interface{}) ([]byte,
 		return nil, err
 		return nil, err
 	}
 	}
 	req.Header.Set("Content-Type", "application/json;charset=utf-8")
 	req.Header.Set("Content-Type", "application/json;charset=utf-8")
-	response, err := http.DefaultClient.Do(req)
+	response, err := DefaultHTTPClient.Do(req)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -129,7 +132,7 @@ func PostJSONWithRespContentType(uri string, obj interface{}) ([]byte, string, e
 		return nil, "", err
 		return nil, "", err
 	}
 	}
 
 
-	response, err := http.Post(uri, "application/json;charset=utf-8", jsonBuf)
+	response, err := DefaultHTTPClient.Post(uri, "application/json;charset=utf-8", jsonBuf)
 	if err != nil {
 	if err != nil {
 		return nil, "", err
 		return nil, "", err
 	}
 	}
@@ -205,7 +208,7 @@ func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte
 	contentType := bodyWriter.FormDataContentType()
 	contentType := bodyWriter.FormDataContentType()
 	bodyWriter.Close()
 	bodyWriter.Close()
 
 
-	resp, e := http.Post(uri, contentType, bodyBuf)
+	resp, e := DefaultHTTPClient.Post(uri, contentType, bodyBuf)
 	if e != nil {
 	if e != nil {
 		err = e
 		err = e
 		return
 		return
@@ -229,7 +232,7 @@ func PostXML(uri string, obj interface{}) ([]byte, error) {
 	}
 	}
 
 
 	body := bytes.NewBuffer(xmlData)
 	body := bytes.NewBuffer(xmlData)
-	response, err := http.Post(uri, "application/xml;charset=utf-8", body)
+	response, err := DefaultHTTPClient.Post(uri, "application/xml;charset=utf-8", body)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -252,11 +255,10 @@ func httpWithTLS(rootCa, key string) (*http.Client, error) {
 	config := &tls.Config{
 	config := &tls.Config{
 		Certificates: []tls.Certificate{cert},
 		Certificates: []tls.Certificate{cert},
 	}
 	}
-	tr := &http.Transport{
-		TLSClientConfig:    config,
-		DisableCompression: true,
-	}
-	client = &http.Client{Transport: tr}
+	trans := (DefaultHTTPClient.Transport.(*http.Transport)).Clone()
+	trans.TLSClientConfig = config
+	trans.DisableCompression = true
+	client = &http.Client{Transport: trans}
 	return client, nil
 	return client, nil
 }
 }
 
 

+ 7 - 0
wechat.go

@@ -1,6 +1,7 @@
 package wechat
 package wechat
 
 
 import (
 import (
+	"net/http"
 	"os"
 	"os"
 
 
 	log "github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
@@ -14,6 +15,7 @@ import (
 	openConfig "github.com/silenceper/wechat/v2/openplatform/config"
 	openConfig "github.com/silenceper/wechat/v2/openplatform/config"
 	"github.com/silenceper/wechat/v2/pay"
 	"github.com/silenceper/wechat/v2/pay"
 	payConfig "github.com/silenceper/wechat/v2/pay/config"
 	payConfig "github.com/silenceper/wechat/v2/pay/config"
+	"github.com/silenceper/wechat/v2/util"
 	"github.com/silenceper/wechat/v2/work"
 	"github.com/silenceper/wechat/v2/work"
 	workConfig "github.com/silenceper/wechat/v2/work/config"
 	workConfig "github.com/silenceper/wechat/v2/work/config"
 )
 )
@@ -81,3 +83,8 @@ func (wc *Wechat) GetWork(cfg *workConfig.Config) *work.Work {
 	}
 	}
 	return work.NewWork(cfg)
 	return work.NewWork(cfg)
 }
 }
+
+// SetHTTPClient  设置HTTPClient
+func (wc *Wechat) SetHTTPClient(client *http.Client) {
+	util.DefaultHTTPClient = client
+}

+ 387 - 0
work/checkin/checkin.go

@@ -0,0 +1,387 @@
+package checkin
+
+import (
+	"fmt"
+
+	"github.com/silenceper/wechat/v2/util"
+)
+
+const (
+	// setScheduleListURL 为打卡人员排班
+	setScheduleListURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/setcheckinschedulist?access_token=%s"
+	// punchCorrectionURL 为打卡人员补卡
+	punchCorrectionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/punch_correction?access_token=%s"
+	// addUserFaceURL 录入打卡人员人脸信息
+	addUserFaceURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/addcheckinuserface?access_token=%s"
+	// addOptionURL 创建打卡规则
+	addOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/add_checkin_option?access_token=%s"
+	// updateOptionURL 修改打卡规则
+	updateOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/update_checkin_option?access_token=%s"
+	// clearOptionURL 清空打卡规则数组元素
+	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"
+)
+
+// SetScheduleListRequest 为打卡人员排班请求
+type SetScheduleListRequest struct {
+	GroupID   int64                 `json:"groupid"`
+	Items     []SetScheduleListItem `json:"items"`
+	YearMonth int64                 `json:"yearmonth"`
+}
+
+// SetScheduleListItem 排班表信息
+type SetScheduleListItem struct {
+	UserID     string `json:"userid"`
+	Day        int64  `json:"day"`
+	ScheduleID int64  `json:"schedule_id"`
+}
+
+// SetScheduleList 为打卡人员排班
+// see https://developer.work.weixin.qq.com/document/path/93385
+func (r *Client) SetScheduleList(req *SetScheduleListRequest) 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(setScheduleListURL, accessToken), req); err != nil {
+		return err
+	}
+	return util.DecodeWithCommonError(response, "SetScheduleList")
+}
+
+// PunchCorrectionRequest 为打卡人员补卡请求
+type PunchCorrectionRequest struct {
+	UserID              string `json:"userid"`
+	ScheduleDateTime    int64  `json:"schedule_date_time"`
+	ScheduleCheckinTime int64  `json:"schedule_checkin_time"`
+	CheckinTime         int64  `json:"checkin_time"`
+	Remark              string `json:"remark"`
+}
+
+// PunchCorrection 为打卡人员补卡
+// see https://developer.work.weixin.qq.com/document/path/95803
+func (r *Client) PunchCorrection(req *PunchCorrectionRequest) 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(punchCorrectionURL, accessToken), req); err != nil {
+		return err
+	}
+	return util.DecodeWithCommonError(response, "PunchCorrection")
+}
+
+// AddUserFaceRequest 录入打卡人员人脸信息请求
+type AddUserFaceRequest struct {
+	UserID   string `json:"userid"`
+	UserFace string `json:"userface"`
+}
+
+// AddUserFace 录入打卡人员人脸信息
+// see https://developer.work.weixin.qq.com/document/path/93378
+func (r *Client) AddUserFace(req *AddUserFaceRequest) 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(addUserFaceURL, accessToken), req); err != nil {
+		return err
+	}
+	return util.DecodeWithCommonError(response, "AddUserFace")
+}
+
+// AddOptionRequest 创建打卡规则请求
+type AddOptionRequest struct {
+	EffectiveNow bool            `json:"effective_now,omitempty"`
+	Group        OptionGroupRule `json:"group,omitempty"`
+}
+
+// OptionGroupRule 打卡规则字段
+type OptionGroupRule struct {
+	GroupID                int64                        `json:"groupid,omitempty"`
+	GroupType              int64                        `json:"grouptype"`
+	GroupName              string                       `json:"groupname"`
+	CheckinDate            []OptionGroupRuleCheckinDate `json:"checkindate,omitempty"`
+	SpeWorkdays            []OptionGroupSpeWorkdays     `json:"spe_workdays,omitempty"`
+	SpeOffDays             []OptionGroupSpeOffDays      `json:"spe_offdays,omitempty"`
+	SyncHolidays           bool                         `json:"sync_holidays,omitempty"`
+	NeedPhoto              bool                         `json:"need_photo,omitempty"`
+	NoteCanUseLocalPic     bool                         `json:"note_can_use_local_pic,omitempty"`
+	WifiMacInfos           []OptionGroupWifiMacInfos    `json:"wifimac_infos,omitempty"`
+	LocInfos               []OptionGroupLocInfos        `json:"loc_infos,omitempty"`
+	AllowCheckinOffWorkday bool                         `json:"allow_checkin_offworkday,omitempty"`
+	AllowApplyOffWorkday   bool                         `json:"allow_apply_offworkday,omitempty"`
+	Range                  []OptionGroupRange           `json:"range"`
+	WhiteUsers             []string                     `json:"white_users,omitempty"`
+	Type                   int64                        `json:"type,omitempty"`
+	ReporterInfo           OptionGroupReporterInfo      `json:"reporterinfo,omitempty"`
+	AllowApplyBkCnt        int64                        `json:"allow_apply_bk_cnt,omitempty"`
+	AllowApplyBkDayLimit   int64                        `json:"allow_apply_bk_day_limit,omitempty"`
+	BukaLimitNextMonth     int64                        `json:"buka_limit_next_month,omitempty"`
+	OptionOutRange         int64                        `json:"option_out_range,omitempty"`
+	ScheduleList           []OptionGroupScheduleList    `json:"schedulelist,omitempty"`
+	OffWorkIntervalTime    int64                        `json:"offwork_interval_time,omitempty"`
+	UseFaceDetect          bool                         `json:"use_face_detect,omitempty"`
+	OpenFaceLiveDetect     bool                         `json:"open_face_live_detect,omitempty"`
+	OtInfoV2               OptionGroupOtInfoV2          `json:"ot_info_v2,omitempty"`
+	SyncOutCheckin         bool                         `json:"sync_out_checkin,omitempty"`
+	BukaRemind             OptionGroupBukaRemind        `json:"buka_remind,omitempty"`
+	BukaRestriction        int64                        `json:"buka_restriction,omitempty"`
+	SpanDayTime            int64                        `json:"span_day_time,omitempty"`
+	StandardWorkDuration   int64                        `json:"standard_work_duration,omitempty"`
+}
+
+// OptionGroupRuleCheckinDate 固定时间上下班打卡时间
+type OptionGroupRuleCheckinDate struct {
+	Workdays            []int64                      `json:"workdays"`
+	CheckinTime         []OptionGroupRuleCheckinTime `json:"checkintime"`
+	FlexTime            int64                        `json:"flex_time"`
+	AllowFlex           bool                         `json:"allow_flex"`
+	FlexOnDutyTime      int64                        `json:"flex_on_duty_time"`
+	FlexOffDutyTime     int64                        `json:"flex_off_duty_time"`
+	MaxAllowArriveEarly int64                        `json:"max_allow_arrive_early"`
+	MaxAllowArriveLate  int64                        `json:"max_allow_arrive_late"`
+	LateRule            OptionGroupLateRule          `json:"late_rule"`
+}
+
+// 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"`
+}
+
+// OptionGroupLateRule 晚走晚到时间规则信息
+type OptionGroupLateRule struct {
+	OffWorkAfterTime      int64                 `json:"offwork_after_time"`
+	OnWorkFlexTime        int64                 `json:"onwork_flex_time"`
+	AllowOffWorkAfterTime int64                 `json:"allow_offwork_after_time"`
+	TimeRules             []OptionGroupTimeRule `json:"timerules"`
+}
+
+// OptionGroupTimeRule 晚走晚到时间规则
+type OptionGroupTimeRule struct {
+	OffWorkAfterTime int64 `json:"offwork_after_time"`
+	OnWorkFlexTime   int64 `json:"onwork_flex_time"`
+}
+
+// OptionGroupSpeWorkdays 特殊工作日
+type OptionGroupSpeWorkdays struct {
+	Timestamp   int64                    `json:"timestamp"`
+	Notes       string                   `json:"notes"`
+	CheckinTime []OptionGroupCheckinTime `json:"checkintime"`
+	Type        int64                    `json:"type"`
+	BegTime     int64                    `json:"begtime"`
+	EndTime     int64                    `json:"endtime"`
+}
+
+// OptionGroupCheckinTime 特殊工作日的上下班打卡时间配置
+type OptionGroupCheckinTime 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"`
+}
+
+// OptionGroupSpeOffDays 特殊非工作日
+type OptionGroupSpeOffDays struct {
+	Timestamp int64  `json:"timestamp"`
+	Notes     string `json:"notes"`
+	Type      int64  `json:"type"`
+	BegTime   int64  `json:"begtime"`
+	EndTime   int64  `json:"endtime"`
+}
+
+// OptionGroupWifiMacInfos WIFI信息
+type OptionGroupWifiMacInfos struct {
+	WifiName string `json:"wifiname"`
+	WifiMac  string `json:"wifimac"`
+}
+
+// OptionGroupLocInfos 地点信息
+type OptionGroupLocInfos struct {
+	Lat       int64  `json:"lat"`
+	Lng       int64  `json:"lng"`
+	LocTitle  string `json:"loc_title"`
+	LocDetail string `json:"loc_detail"`
+	Distance  int64  `json:"distance"`
+}
+
+// OptionGroupRange 人员信息
+type OptionGroupRange struct {
+	PartyID []string `json:"party_id"`
+	UserID  []string `json:"userid"`
+	TagID   []int64  `json:"tagid"`
+}
+
+// OptionGroupReporterInfo 汇报人
+type OptionGroupReporterInfo struct {
+	Reporters []OptionGroupReporters `json:"reporters"`
+}
+
+// OptionGroupReporters 汇报对象
+type OptionGroupReporters struct {
+	UserID string `json:"userid"`
+	TagID  int64  `json:"tagid"`
+}
+
+// OptionGroupScheduleList 自定义排班规则所有排班
+type OptionGroupScheduleList struct {
+	ScheduleID          int64                    `json:"schedule_id"`
+	ScheduleName        string                   `json:"schedule_name"`
+	TimeSection         []OptionGroupTimeSection `json:"time_section"`
+	AllowFlex           bool                     `json:"allow_flex"`
+	FlexOnDutyTime      int64                    `json:"flex_on_duty_time"`
+	FlexOffDutyTime     int64                    `json:"flex_off_duty_time"`
+	LateRule            OptionGroupLateRule      `json:"late_rule"`
+	MaxAllowArriveEarly int64                    `json:"max_allow_arrive_early"`
+	MaxAllowArriveLate  int64                    `json:"max_allow_arrive_late"`
+}
+
+// OptionGroupTimeSection 班次上下班时段信息
+type OptionGroupTimeSection 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"`
+	RestBeginTime      int64 `json:"rest_begin_time"`
+	RestEndTime        int64 `json:"rest_end_time"`
+	AllowRest          bool  `json:"allow_rest"`
+	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"`
+}
+
+// OptionGroupOtInfoV2 加班配置
+type OptionGroupOtInfoV2 struct {
+	WorkdayConf OptionGroupWorkdayConf `json:"workdayconf"`
+}
+
+// OptionGroupWorkdayConf 工作日加班配置
+type OptionGroupWorkdayConf struct {
+	AllowOt bool  `json:"allow_ot"`
+	Type    int64 `json:"type"`
+}
+
+// OptionGroupBukaRemind 补卡提醒
+type OptionGroupBukaRemind struct {
+	OpenRemind      bool  `json:"open_remind"`
+	BukaRemindDay   int64 `json:"buka_remind_day"`
+	BukaRemindMonth int64 `json:"buka_remind_month"`
+}
+
+// AddOption 创建打卡规则
+// see https://developer.work.weixin.qq.com/document/path/98041#%E5%88%9B%E5%BB%BA%E6%89%93%E5%8D%A1%E8%A7%84%E5%88%99
+func (r *Client) AddOption(req *AddOptionRequest) 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(addOptionURL, accessToken), req); err != nil {
+		return err
+	}
+	return util.DecodeWithCommonError(response, "AddOption")
+}
+
+// UpdateOptionRequest 修改打卡规则请求
+type UpdateOptionRequest struct {
+	EffectiveNow bool            `json:"effective_now,omitempty"`
+	Group        OptionGroupRule `json:"group,omitempty"`
+}
+
+// UpdateOption 修改打卡规则
+// see https://developer.work.weixin.qq.com/document/path/98041#%E4%BF%AE%E6%94%B9%E6%89%93%E5%8D%A1%E8%A7%84%E5%88%99
+func (r *Client) UpdateOption(req *UpdateOptionRequest) 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(updateOptionURL, accessToken), req); err != nil {
+		return err
+	}
+	return util.DecodeWithCommonError(response, "UpdateOption")
+}
+
+// ClearOptionRequest 清空打卡规则数组元素请求
+type ClearOptionRequest struct {
+	GroupID      int64   `json:"groupid"`
+	ClearField   []int64 `json:"clear_field"`
+	EffectiveNow bool    `json:"effective_now"`
+}
+
+// ClearOption 清空打卡规则数组元素
+// see https://developer.work.weixin.qq.com/document/path/98041#%E6%B8%85%E7%A9%BA%E6%89%93%E5%8D%A1%E8%A7%84%E5%88%99%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0
+func (r *Client) ClearOption(req *ClearOptionRequest) 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(clearOptionURL, accessToken), req); err != nil {
+		return err
+	}
+	return util.DecodeWithCommonError(response, "ClearOption")
+}
+
+// DelOptionRequest 删除打卡规则请求
+type DelOptionRequest struct {
+	GroupID      int64 `json:"groupid"`
+	EffectiveNow bool  `json:"effective_now"`
+}
+
+// DelOption 删除打卡规则
+// see https://developer.work.weixin.qq.com/document/path/98041#%E5%88%A0%E9%99%A4%E6%89%93%E5%8D%A1%E8%A7%84%E5%88%99
+func (r *Client) DelOption(req *DelOptionRequest) 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(delOptionURL, accessToken), req); err != nil {
+		return err
+	}
+	return util.DecodeWithCommonError(response, "DelOption")
+}

+ 50 - 10
work/externalcontact/external_user.go

@@ -63,16 +63,16 @@ type ExternalUserDetailResponse struct {
 
 
 // ExternalUser 外部联系人
 // ExternalUser 外部联系人
 type ExternalUser struct {
 type ExternalUser struct {
-	ExternalUserID  string `json:"external_userid"`
-	Name            string `json:"name"`
-	Avatar          string `json:"avatar"`
-	Type            int64  `json:"type"`
-	Gender          int64  `json:"gender"`
-	UnionID         string `json:"unionid"`
-	Position        string `json:"position"`
-	CorpName        string `json:"corp_name"`
-	CorpFullName    string `json:"corp_full_name"`
-	ExternalProfile string `json:"external_profile"`
+	ExternalUserID  string           `json:"external_userid"`
+	Name            string           `json:"name"`
+	Avatar          string           `json:"avatar"`
+	Type            int64            `json:"type"`
+	Gender          int64            `json:"gender"`
+	UnionID         string           `json:"unionid"`
+	Position        string           `json:"position"`
+	CorpName        string           `json:"corp_name"`
+	CorpFullName    string           `json:"corp_full_name"`
+	ExternalProfile *ExternalProfile `json:"external_profile,omitempty"`
 }
 }
 
 
 // FollowUser 跟进用户(指企业内部用户)
 // FollowUser 跟进用户(指企业内部用户)
@@ -104,6 +104,46 @@ type WechatChannel struct {
 	Source   int    `json:"source"`
 	Source   int    `json:"source"`
 }
 }
 
 
+// ExternalProfile 外部联系人的自定义展示信息,可以有多个字段和多种类型,包括文本,网页和小程序
+type ExternalProfile struct {
+	ExternalCorpName string         `json:"external_corp_name"`
+	WechatChannels   WechatChannels `json:"wechat_channels"`
+	ExternalAttr     []ExternalAttr `json:"external_attr"`
+}
+
+// WechatChannels 视频号属性。须从企业绑定到企业微信的视频号中选择,可在“我的企业”页中查看绑定的视频号
+type WechatChannels struct {
+	Nickname string `json:"nickname"`
+	Status   int    `json:"status"`
+}
+
+// ExternalAttr 属性列表,目前支持文本、网页、小程序三种类型
+type ExternalAttr struct {
+	Type        int          `json:"type"`
+	Name        string       `json:"name"`
+	Text        *Text        `json:"text,omitempty"`
+	Web         *Web         `json:"web,omitempty"`
+	MiniProgram *MiniProgram `json:"miniprogram,omitempty"`
+}
+
+// Text 文本
+type Text struct {
+	Value string `json:"value"`
+}
+
+// Web 网页
+type Web struct {
+	URL   string `json:"url"`
+	Title string `json:"title"`
+}
+
+// MiniProgram 小程序
+type MiniProgram struct {
+	AppID    string `json:"appid"`
+	Pagepath string `json:"pagepath"`
+	Title    string `json:"title"`
+}
+
 // GetExternalUserDetail 获取外部联系人详情
 // GetExternalUserDetail 获取外部联系人详情
 // @see https://developer.work.weixin.qq.com/document/path/92114
 // @see https://developer.work.weixin.qq.com/document/path/92114
 func (r *Client) GetExternalUserDetail(externalUserID string, nextCursor ...string) (*ExternalUserDetailResponse, error) {
 func (r *Client) GetExternalUserDetail(externalUserID string, nextCursor ...string) (*ExternalUserDetailResponse, error) {

+ 8 - 7
work/externalcontact/groupchat.go

@@ -76,13 +76,14 @@ type (
 	}
 	}
 	//GroupChat 客户群详情
 	//GroupChat 客户群详情
 	GroupChat struct {
 	GroupChat struct {
-		ChatID     string            `json:"chat_id"`     //客户群ID
-		Name       string            `json:"name"`        //群名
-		Owner      string            `json:"owner"`       //群主ID
-		CreateTime int64             `json:"create_time"` //群的创建时间
-		Notice     string            `json:"notice"`      //群公告
-		MemberList []GroupChatMember `json:"member_list"` //群成员列表
-		AdminList  []GroupChatAdmin  `json:"admin_list"`  //群管理员列表
+		ChatID        string            `json:"chat_id"`        //客户群ID
+		Name          string            `json:"name"`           //群名
+		Owner         string            `json:"owner"`          //群主ID
+		CreateTime    int64             `json:"create_time"`    //群的创建时间
+		Notice        string            `json:"notice"`         //群公告
+		MemberList    []GroupChatMember `json:"member_list"`    //群成员列表
+		AdminList     []GroupChatAdmin  `json:"admin_list"`     //群管理员列表
+		MemberVersion string            `json:"member_version"` //当前群成员版本号。可以配合客户群变更事件减少主动调用本接口的次数
 	}
 	}
 	//GroupChatDetailResponse 客户群详情 返回值
 	//GroupChatDetailResponse 客户群详情 返回值
 	GroupChatDetailResponse struct {
 	GroupChatDetailResponse struct {