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

Compare commits

..

2 Commits

Author SHA1 Message Date
houseme
d1cee8868e feat: add order setter relation 2023-09-22 18:08:40 +08:00
houseme
aa1afc5a35 improve code 2023-09-22 17:28:17 +08:00
14 changed files with 78 additions and 319 deletions

View File

@@ -66,9 +66,8 @@ func (ak *DefaultAccessToken) GetAccessToken() (accessToken string, err error) {
func (ak *DefaultAccessToken) GetAccessTokenContext(ctx context.Context) (accessToken string, err error) {
// 先从cache中取
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.appID)
val := ak.cache.Get(accessTokenCacheKey)
if accessToken = val.(string); accessToken != "" {
return
if val := ak.cache.Get(accessTokenCacheKey); val != nil {
return val.(string), nil
}
// 加上lock是为了防止在并发获取token时cache刚好失效导致从微信服务器上获取到不同token
@@ -76,9 +75,8 @@ func (ak *DefaultAccessToken) GetAccessTokenContext(ctx context.Context) (access
defer ak.accessTokenLock.Unlock()
// 双检,防止重复从微信服务器获取
val = ak.cache.Get(accessTokenCacheKey)
if accessToken = val.(string); accessToken != "" {
return
if val := ak.cache.Get(accessTokenCacheKey); val != nil {
return val.(string), nil
}
// cache失效从微信服务器获取

View File

@@ -15,6 +15,8 @@ const (
checkEncryptedDataURL = "https://api.weixin.qq.com/wxa/business/checkencryptedmsg?access_token=%s"
getPhoneNumber = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s"
checkSessionURL = "https://api.weixin.qq.com/wxa/checksession?access_token=%s&openid=%s&signature=%s&sig_method=hmac_sha256"
)
// Auth 登录/用户信息
@@ -33,7 +35,7 @@ type ResCode2Session struct {
OpenID string `json:"openid"` // 用户唯一标识
SessionKey string `json:"session_key"` // 会话密钥
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符在满足UnionID下发条件的情况下会返回
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符,在满足 UnionID 下发条件的情况下会返回
}
// RspCheckEncryptedData .
@@ -70,12 +72,12 @@ func (auth *Auth) GetPaidUnionID() {
// TODO
}
// CheckEncryptedData .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据
// CheckEncryptedData .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近 3 天生成的加密数据
func (auth *Auth) CheckEncryptedData(encryptedMsgHash string) (result RspCheckEncryptedData, err error) {
return auth.CheckEncryptedDataContext(context2.Background(), encryptedMsgHash)
}
// CheckEncryptedDataContext .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据
// CheckEncryptedDataContext .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近 3 天生成的加密数据
func (auth *Auth) CheckEncryptedDataContext(ctx context2.Context, encryptedMsgHash string) (result RspCheckEncryptedData, err error) {
var response []byte
var (
@@ -85,7 +87,7 @@ func (auth *Auth) CheckEncryptedDataContext(ctx context2.Context, encryptedMsgHa
return
}
// 由于GetPhoneNumberContext需要传入JSON所以HTTPPostContext入参改为[]byte
// 由于 GetPhoneNumberContext 需要传入 JSON所以 HTTPPostContext 入参改为 []byte
if response, err = util.HTTPPostContext(ctx, fmt.Sprintf(checkEncryptedDataURL, at), []byte("encrypted_msg_hash="+encryptedMsgHash), nil); err != nil {
return
}
@@ -113,38 +115,62 @@ type PhoneInfo struct {
} `json:"watermark"` // 数据水印
}
// GetPhoneNumberContext 小程序通过code获取用户手机号
func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*GetPhoneNumberResponse, error) {
var response []byte
var (
at string
err error
)
if at, err = auth.GetAccessToken(); err != nil {
// GetPhoneNumberContext 小程序通过 code 获取用户手机号
func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (result *GetPhoneNumberResponse, err error) {
var accessToken string
if accessToken, err = auth.GetAccessToken(); err != nil {
return nil, err
}
body := map[string]interface{}{
"code": code,
}
bodyBytes, err := json.Marshal(body)
bodyBytes, err := json.Marshal(map[string]interface{}{
"code": code,
})
if err != nil {
return nil, err
}
header := map[string]string{"Content-Type": "application/json;charset=utf-8"}
if response, err = util.HTTPPostContext(ctx, fmt.Sprintf(getPhoneNumber, at), bodyBytes, header); err != nil {
var (
header = map[string]string{"Content-Type": "application/json;charset=utf-8"}
response []byte
)
if response, err = util.HTTPPostContext(ctx, fmt.Sprintf(getPhoneNumber, accessToken), bodyBytes, header); err != nil {
return nil, err
}
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
}
// GetPhoneNumber 小程序通过code获取用户手机号
// GetPhoneNumber 小程序通过 code 获取用户手机号
func (auth *Auth) GetPhoneNumber(code string) (*GetPhoneNumberResponse, error) {
return auth.GetPhoneNumberContext(context2.Background(), code)
}
// // CheckSession 检验登录态是否过期。
// func (auth *Auth) CheckSession(sessionKey, openID string) (result *CheckSessionResponse, err error) {
// return auth.CheckSessionContext(context2.Background(), sessionKey, openID)
// }
//
// // CheckSessionContext 检验登录态是否过期。
// func (auth *Auth) CheckSessionContext(ctx context2.Context, sessionKey, openID string) (result *CheckSessionResponse, err error) {
// var accessToken string
// if accessToken, err = auth.GetAccessToken(); err != nil {
// return nil, err
// }
// var (
// response []byte
// signature string = sessionKey
// )
// if response, err = util.HTTPGetContext(ctx, fmt.Sprintf(checkSessionURL, accessToken, openID, signature)); err != nil {
// return nil, err
// }
//
// err = util.DecodeWithError(response, &result, "CheckSessionContext")
// return
// }
//
// // CheckSessionResponse 检验登录态是否过期。
// type CheckSessionResponse struct {
// util.CommonError
// }

View File

@@ -117,6 +117,24 @@ const (
// queryPublishGoods 查询批量发布道具任务状态
queryPublishGoods = "/xpay/query_publish_goods"
// 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"
// defaultUnifiedOrderURL default unified order url
defaultUnifiedOrderURL = "requestVirtualPayment"
)
const (

View File

@@ -145,6 +145,8 @@ type OrderItem struct {
WxOrderID string `json:"wx_order_id"` // 微信内部单号
ChannelOrderID string `json:"channel_order_id"` // 渠道订单号,为用户微信支付详情页面上的商户单号
WxPayOrderID string `json:"wxpay_order_id"` // 微信支付交易单号,为用户微信支付详情页面上的交易单号
SettTime int64 `json:"sett_time"` // 结算时间unix 秒级时间戳,结算时间的秒级时间戳,大于 0 表示结算成功
SettState uint `json:"sett_state"` // 结算状态 0-未开始结算 1-结算中 2-结算成功
}
// QueryOrderResponse 查询创建的订单(现金单,非代币单)响应参数

View File

@@ -479,6 +479,7 @@ func (s *VirtualPayment) requestAddress(params URLParams) (url string, err error
case queryUserBalance:
case currencyPay:
case cancelCurrencyPay:
case defaultUnifiedOrderURL:
if params.PaySign, params.Signature, err = s.PaySignature(params.Path, params.Content); err != nil {
return
}

View File

@@ -4,7 +4,6 @@ import (
"github.com/silenceper/wechat/v2/pay/config"
"github.com/silenceper/wechat/v2/pay/notify"
"github.com/silenceper/wechat/v2/pay/order"
"github.com/silenceper/wechat/v2/pay/redpacket"
"github.com/silenceper/wechat/v2/pay/refund"
"github.com/silenceper/wechat/v2/pay/transfer"
)
@@ -38,8 +37,3 @@ func (pay *Pay) GetRefund() *refund.Refund {
func (pay *Pay) GetTransfer() *transfer.Transfer {
return transfer.NewTransfer(pay.cfg)
}
// GetRedpacket 红包
func (pay *Pay) GetRedpacket() *redpacket.Redpacket {
return redpacket.NewRedpacket(pay.cfg)
}

View File

@@ -1,131 +0,0 @@
package redpacket
import (
"encoding/xml"
"fmt"
"strconv"
"github.com/silenceper/wechat/v2/pay/config"
"github.com/silenceper/wechat/v2/util"
)
// redpacketGateway 发放红包接口
// https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_4&index=3
var redpacketGateway = "https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack"
// Redpacket struct extends context
type Redpacket struct {
*config.Config
}
// NewRedpacket return an instance of Redpacket package
func NewRedpacket(cfg *config.Config) *Redpacket {
return &Redpacket{cfg}
}
// Params 调用参数
type Params struct {
MchBillno string // 商户订单号
SendName string // 商户名称
ReOpenID string
TotalAmount int
TotalNum int
Wishing string
ClientIP string
ActName string
Remark string
RootCa string // ca证书
}
// request 接口请求参数
type request struct {
NonceStr string `xml:"nonce_str"`
Sign string `xml:"sign"`
MchID string `xml:"mch_id"`
MchBillno string `xml:"mch_billno"`
Wxappid string `xml:"wxappid"`
SendName string `xml:"send_name"`
ReOpenID string `xml:"re_openid"`
TotalAmount int `xml:"total_amount"`
TotalNum int `xml:"total_num"`
Wishing string `xml:"wishing"`
ClientIP string `xml:"client_ip"`
ActName string `xml:"act_name"`
Remark string `xml:"remark"`
}
// Response 接口返回
type Response struct {
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
ResultCode string `xml:"result_code,omitempty"`
ErrCode string `xml:"err_code,omitempty"`
ErrCodeDes string `xml:"err_code_des,omitempty"`
MchBillno string `xml:"mch_billno,omitempty"`
MchID string `xml:"mch_id,omitempty"`
Wxappid string `xml:"wxappid"`
ReOpenID string `xml:"re_openid"`
TotalAmount int `xml:"total_amount"`
SendListid string `xml:"send_listid"`
}
// SendRedpacket 发放红包
func (redpacket *Redpacket) SendRedpacket(p *Params) (rsp *Response, err error) {
nonceStr := util.RandomStr(32)
param := make(map[string]string)
param["nonce_str"] = nonceStr
param["mch_id"] = redpacket.MchID
param["wxappid"] = redpacket.AppID
param["mch_billno"] = p.MchBillno
param["send_name"] = p.SendName
param["re_openid"] = p.ReOpenID
param["total_amount"] = strconv.Itoa(p.TotalAmount)
param["total_num"] = strconv.Itoa(p.TotalNum)
param["wishing"] = p.Wishing
param["client_ip"] = p.ClientIP
param["act_name"] = p.ActName
param["remark"] = p.Remark
//param["scene_id"] = "PRODUCT_2"
sign, err := util.ParamSign(param, redpacket.Key)
if err != nil {
return
}
req := request{
NonceStr: nonceStr,
Sign: sign,
MchID: redpacket.MchID,
Wxappid: redpacket.AppID,
MchBillno: p.MchBillno,
SendName: p.SendName,
ReOpenID: p.ReOpenID,
TotalAmount: p.TotalAmount,
TotalNum: p.TotalNum,
Wishing: p.Wishing,
ClientIP: p.ClientIP,
ActName: p.ActName,
Remark: p.Remark,
}
rawRet, err := util.PostXMLWithTLS(redpacketGateway, req, p.RootCa, redpacket.MchID)
if err != nil {
return
}
err = xml.Unmarshal(rawRet, &rsp)
if err != nil {
return
}
if rsp.ReturnCode == "SUCCESS" {
if rsp.ResultCode == "SUCCESS" {
err = nil
return
}
err = fmt.Errorf("send redpacket error, errcode=%s,errmsg=%s", rsp.ErrCode, rsp.ErrCodeDes)
return
}
err = fmt.Errorf("[msg : xmlUnmarshalError] [rawReturn : %s] [sign : %s]", string(rawRet), sign)
return
}

View File

@@ -13,8 +13,6 @@ const (
departmentSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/department/simplelist?access_token=%s&id=%d"
// departmentListURL 获取部门列表
departmentListURL = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s"
// departmentGetURL 获取单个部门详情 https://qyapi.weixin.qq.com/cgi-bin/department/get?access_token=ACCESS_TOKEN&id=ID
departmentGetURL = "https://qyapi.weixin.qq.com/cgi-bin/department/get?access_token=%s&id=%d"
)
type (
@@ -123,24 +121,3 @@ func (r *Client) DepartmentList() ([]*Department, error) {
// 返回数据
return result.Department, err
}
// DepartmentGet 获取单个部门详情
// see https://developer.work.weixin.qq.com/document/path/95351
func (r *Client) DepartmentGet(departmentID int) (*Department, 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(departmentGetURL, accessToken, departmentID)); err != nil {
return nil, err
}
result := &Department{}
if err = util.DecodeWithError(response, result, "DepartmentGet"); err != nil {
return nil, err
}
return result, nil
}

View File

@@ -221,7 +221,7 @@ type UserGetResponse struct {
} `json:"external_profile"` // 成员对外属性,字段详情见对外属性;代开发自建应用需要管理员授权才返回;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段
}
// UserGet 读取成员
// UserGet 获取部门成员
// @see https://developer.work.weixin.qq.com/document/path/90196
func (r *Client) UserGet(UserID string) (*UserGetResponse, error) {
var (
@@ -237,8 +237,8 @@ func (r *Client) UserGet(UserID string) (*UserGetResponse, error) {
strings.Join([]string{
userGetURL,
util.Query(map[string]interface{}{
"access_token": accessToken,
"userid": UserID,
"access_token": accessToken,
"department_id": UserID,
}),
}, "?")); err != nil {
return nil, err

View File

@@ -1,69 +0,0 @@
package checkin
import (
"fmt"
"github.com/silenceper/wechat/v2/util"
)
const (
// getCheckinDataURL 获取打卡记录数据
getCheckinDataURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/getcheckindata?access_token=%s"
)
type (
// GetCheckinDataRequest 获取打卡记录数据请求
GetCheckinDataRequest struct {
OpenCheckinDataType int64 `json:"opencheckindatatype"`
StartTime int64 `json:"starttime"`
EndTime int64 `json:"endtime"`
UserIDList []string `json:"useridlist"`
}
// GetCheckinDataResponse 获取打卡记录数据响应
GetCheckinDataResponse struct {
util.CommonError
CheckinData []*GetCheckinDataItem `json:"checkindata"`
}
// GetCheckinDataItem 打卡记录数据
GetCheckinDataItem struct {
UserID string `json:"userid"`
GroupName string `json:"groupname"`
CheckinType string `json:"checkin_type"`
ExceptionType string `json:"exception_type"`
CheckinTime int64 `json:"checkin_time"`
LocationTitle string `json:"location_title"`
LocationDetail string `json:"location_detail"`
WifiName string `json:"wifiname"`
Notes string `json:"notes"`
WifiMac string `json:"wifimac"`
MediaIDs []string `json:"mediaids"`
SchCheckinTime int64 `json:"sch_checkin_time"`
GroupID int64 `json:"groupid"`
ScheduleID int64 `json:"schedule_id"`
TimelineID int64 `json:"timeline_id"`
Lat int64 `json:"lat,omitempty"`
Lng int64 `json:"lng,omitempty"`
DeviceID string `json:"deviceid,omitempty"`
}
)
// GetCheckinData 获取打卡记录数据
// @see https://developer.work.weixin.qq.com/document/path/90262
func (r *Client) GetCheckinData(req *GetCheckinDataRequest) (*GetCheckinDataResponse, 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(getCheckinDataURL, accessToken), req); err != nil {
return nil, err
}
result := &GetCheckinDataResponse{}
if err = util.DecodeWithError(response, result, "GetCheckinData"); err != nil {
return nil, err
}
return result, nil
}

View File

@@ -1,17 +0,0 @@
package checkin
import (
"github.com/silenceper/wechat/v2/work/context"
)
// Client 打卡接口实例
type Client struct {
*context.Context
}
// NewClient 初始化实例
func NewClient(ctx *context.Context) *Client {
return &Client{
ctx,
}
}

View File

@@ -70,7 +70,6 @@ type (
GroupNickname string `json:"group_nickname"` //在群里的昵称
Name string `json:"name"` //名字。仅当 need_name = 1 时返回 如果是微信用户,则返回其在微信中设置的名字 如果是企业微信联系人,则返回其设置对外展示的别名或实名
UnionID string `json:"unionid,omitempty"` //外部联系人在微信开放平台的唯一身份标识微信unionid通过此字段企业可将外部联系人与公众号/小程序用户关联起来。仅当群成员类型是微信用户包括企业成员未添加好友且企业绑定了微信开发者ID有此字段查看绑定方法。第三方不可获取上游企业不可获取下游企业客户的unionid字段
State string `json:"state,omitempty"` //如果在配置入群方式时配置了state参数那么在获取客户群详情时通过该方式入群的成员会额外获取到相应的state参数
}
//GroupChatAdmin 群管理员
GroupChatAdmin struct {

View File

@@ -11,8 +11,6 @@ const (
uploadImgURL = "https://qyapi.weixin.qq.com/cgi-bin/media/uploadimg?access_token=%s"
// uploadTempFile 上传临时素材
uploadTempFile = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s"
// uploadAttachment 上传附件资源
uploadAttachment = "https://qyapi.weixin.qq.com/cgi-bin/media/upload_attachment?access_token=%s&media_type=%s&attachment_type=%d"
)
// UploadImgResponse 上传图片响应
@@ -29,14 +27,6 @@ type UploadTempFileResponse struct {
Type string `json:"type"`
}
// UploadAttachmentResponse 上传资源附件响应
type UploadAttachmentResponse struct {
util.CommonError
MediaID string `json:"media_id"`
CreateAt int64 `json:"created_at"`
Type string `json:"type"`
}
// UploadImg 上传图片
// @see https://developer.work.weixin.qq.com/document/path/90256
func (r *Client) UploadImg(filename string) (*UploadImgResponse, error) {
@@ -79,26 +69,3 @@ func (r *Client) UploadTempFile(filename string, mediaType string) (*UploadTempF
}
return result, nil
}
// UploadAttachment 上传附件资源
// @see https://developer.work.weixin.qq.com/document/path/95098
// @mediaType 媒体文件类型分别有图片image、视频video、普通文件file
// @attachment_type 附件类型不同的附件类型用于不同的场景。1朋友圈2:商品图册
func (r *Client) UploadAttachment(filename string, mediaType string, attachmentType int) (*UploadAttachmentResponse, error) {
var (
accessToken string
err error
)
if accessToken, err = r.GetAccessToken(); err != nil {
return nil, err
}
var response []byte
if response, err = util.PostFile("media", filename, fmt.Sprintf(uploadAttachment, accessToken, mediaType, attachmentType)); err != nil {
return nil, err
}
result := &UploadAttachmentResponse{}
if err = util.DecodeWithError(response, result, "UploadAttachment"); err != nil {
return nil, err
}
return result, nil
}

View File

@@ -4,7 +4,6 @@ import (
"github.com/silenceper/wechat/v2/credential"
"github.com/silenceper/wechat/v2/work/addresslist"
"github.com/silenceper/wechat/v2/work/appchat"
"github.com/silenceper/wechat/v2/work/checkin"
"github.com/silenceper/wechat/v2/work/config"
"github.com/silenceper/wechat/v2/work/context"
"github.com/silenceper/wechat/v2/work/externalcontact"
@@ -86,8 +85,3 @@ func (wk *Work) GetAppChat() *appchat.Client {
func (wk *Work) GetInvoice() *invoice.Client {
return invoice.NewClient(wk.ctx)
}
// GetCheckin 获取打卡接口实例
func (wk *Work) GetCheckin() *checkin.Client {
return checkin.NewClient(wk.ctx)
}