diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml deleted file mode 100644 index 642f0d8e..00000000 --- a/.github/workflows/coverage.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: coverage - -on: - push: - branches: [ v3 ] - paths-ignore: - - '**.md' - pull_request: - branches: [ v3 ] - paths-ignore: - - '**.md' - -jobs: - test: - name: Test with Coverage - runs-on: ubuntu-latest - steps: - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.17 - - - name: Check out code - uses: actions/checkout@v2 - - - name: Install dependencies - run: | - go mod download - - - name: Run Unit tests - run: | - go test -race -covermode atomic -coverprofile=covprofile ./... - - - name: Install goveralls - run: go install github.com/mattn/goveralls@latest - - - name: Send coverage - env: - COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: goveralls -coverprofile=covprofile -service=github \ No newline at end of file diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml deleted file mode 100644 index 94643082..00000000 --- a/.github/workflows/go.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Go - -on: - push: - branches: [ v3 ] - paths-ignore: - - '**.md' - pull_request: - branches: [ v3 ] - paths-ignore: - - '**.md' - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.17 - - - name: Test - run: go test -v ./... - - - name: Test - run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic - - - name: Coverage - run: bash <(curl -s https://codecov.io/bash) diff --git a/.gitignore b/.gitignore index ca7a6810..a9e49523 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .idea .vscode *.log +.github gitmod.sh /service/*/*_test.go /utils/*/*_test.go diff --git a/service/wechatopen b/service/wechatopen deleted file mode 160000 index 7d3f9e33..00000000 --- a/service/wechatopen +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7d3f9e33ee862f8acd65a4cb8ecffb56c4b860a2 diff --git a/service/wechatopen/aes_crypto.go b/service/wechatopen/aes_crypto.go new file mode 100644 index 00000000..053dc3e8 --- /dev/null +++ b/service/wechatopen/aes_crypto.go @@ -0,0 +1,125 @@ +package wechatopen + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "fmt" +) + +const ( + BLOCK_SIZE = 32 // PKCS#7 + BLOCK_MASK = BLOCK_SIZE - 1 // BLOCK_SIZE 为 2^n 时, 可以用 mask 获取针对 BLOCK_SIZE 的余数 +) + +// 把整数 n 格式化成 4 字节的网络字节序 +func encodeNetworkByteOrder(b []byte, n uint32) { + b[0] = byte(n >> 24) + b[1] = byte(n >> 16) + b[2] = byte(n >> 8) + b[3] = byte(n) +} + +// 从 4 字节的网络字节序里解析出整数 +func decodeNetworkByteOrder(b []byte) (n uint32) { + return uint32(b[0])<<24 | + uint32(b[1])<<16 | + uint32(b[2])<<8 | + uint32(b[3]) +} + +// AESEncryptMsg ciphertext = AES_Encrypt[random(16B) + msg_len(4B) + rawXMLMsg + appId] +func AESEncryptMsg(random, rawXMLMsg []byte, appId string, aesKey []byte) (ciphertext []byte) { + appIdOffset := 20 + len(rawXMLMsg) + contentLen := appIdOffset + len(appId) + amountToPad := BLOCK_SIZE - contentLen&BLOCK_MASK + plaintextLen := contentLen + amountToPad + + plaintext := make([]byte, plaintextLen) + + // 拼接 + copy(plaintext[:16], random) + encodeNetworkByteOrder(plaintext[16:20], uint32(len(rawXMLMsg))) + copy(plaintext[20:], rawXMLMsg) + copy(plaintext[appIdOffset:], appId) + + // PKCS#7 补位 + for i := contentLen; i < plaintextLen; i++ { + plaintext[i] = byte(amountToPad) + } + + // 加密 + block, err := aes.NewCipher(aesKey) + if err != nil { + panic(err) + } + mode := cipher.NewCBCEncrypter(block, aesKey[:16]) + mode.CryptBlocks(plaintext, plaintext) + + ciphertext = plaintext + return +} + +// AESDecryptMsg c解密 +func AESDecryptMsg(decryptStr, aesKey string) (string, error) { + cipherText, err := base64.StdEncoding.DecodeString(decryptStr) + if err != nil { + return "", err + } + + // 解密 + block, err := aes.NewCipher([]byte(aesKey)) + if err != nil { + return "", err + } + + blockMode := cipher.NewCBCDecrypter(block, []byte(aesKey)) + decrypted := make([]byte, len(cipherText)) + blockMode.CryptBlocks(decrypted, cipherText) + + decrypted = pkcs5UnPadding(decrypted) + return string(decrypted), nil +} + +func pkcs5UnPadding(decrypted []byte) []byte { + length := len(decrypted) + unPadding := int(decrypted[length-1]) + return decrypted[:(length - unPadding)] +} + +func AESDecryptData(cipherText, aesKey, iv []byte) (rawData []byte, err error) { + if len(cipherText) < BLOCK_SIZE { + err = fmt.Errorf("the length of ciphertext too short: %d", len(cipherText)) + return + } + + plaintext := make([]byte, len(cipherText)) // len(plaintext) >= BLOCK_SIZE + + // 解密 + block, err := aes.NewCipher(aesKey) + if err != nil { + panic(err) + } + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(plaintext, cipherText) + + // PKCS#7 去除补位 + amountToPad := int(plaintext[len(plaintext)-1]) + if amountToPad < 1 || amountToPad > BLOCK_SIZE { + err = fmt.Errorf("the amount to pad is incorrect: %d", amountToPad) + return + } + plaintext = plaintext[:len(plaintext)-amountToPad] + + // 反拼接 + // len(plaintext) == 16+4+len(rawXMLMsg)+len(appId) + if len(plaintext) <= 20 { + err = fmt.Errorf("plaintext too short, the length is %d", len(plaintext)) + return + } + + rawData = plaintext + + return + +} diff --git a/service/wechatopen/app.go b/service/wechatopen/app.go new file mode 100644 index 00000000..23a84099 --- /dev/null +++ b/service/wechatopen/app.go @@ -0,0 +1,99 @@ +package wechatopen + +import ( + "dtapps/dta/library/utils/gomongo" + "encoding/json" + "errors" + "gitee.com/dtapps/go-library/utils/gohttp" + "gitee.com/dtapps/go-library/utils/gotime" + "gorm.io/gorm" + "net/http" +) + +// App 微信公众号服务 +type App struct { + componentAccessToken string // 第三方平台 access_token + componentVerifyTicket string // 微信后台推送的 ticket + preAuthCode string // 预授权码 + + authorizerAccessToken string // 接口调用令牌 + authorizerRefreshToken string // 刷新令牌 + AuthorizerAppid string // 授权方 appid + + ComponentAppId string // 第三方平台 appid + ComponentAppSecret string // 第三方平台 app_secret + MessageToken string + MessageKey string + + Mongo gomongo.App // 非关系数据库服务 + Db *gorm.DB // 关系数据库服务 +} + +func (app *App) request(url string, params map[string]interface{}, method string) (resp []byte, err error) { + switch method { + case http.MethodGet: + get, err := gohttp.Get(url, params) + // 日志 + go app.mongoLog(url, params, method, get) + return get.Body, err + case http.MethodPost: + // 请求参数 + paramsStr, err := json.Marshal(params) + postJson, err := gohttp.PostJson(url, paramsStr) + // 日志 + go app.mongoLog(url, params, method, postJson) + return postJson.Body, err + default: + return nil, errors.New("请求类型不支持") + } +} + +// GetAuthorizerAccessToken 获取授权方令牌 +func (app *App) GetAuthorizerAccessToken() string { + if app.Db == nil { + return app.authorizerAccessToken + } + var result AuthorizerAccessToken + app.Db.Where("component_app_id = ?", app.ComponentAppId).Where("authorizer_app_id = ?", app.AuthorizerAppid).Where("expire_time >= ?", gotime.Current().Format()).Last(&result) + return result.AuthorizerAccessToken +} + +// GetAuthorizerRefreshToken 获取刷新令牌 +func (app *App) GetAuthorizerRefreshToken() string { + if app.Db == nil { + return app.authorizerRefreshToken + } + var result AuthorizerAccessToken + app.Db.Where("component_app_id = ?", app.ComponentAppId).Where("authorizer_app_id = ?", app.AuthorizerAppid).Last(&result) + return result.AuthorizerRefreshToken +} + +// GetPreAuthCode 获取预授权码 +func (app *App) GetPreAuthCode() string { + if app.Db == nil { + return app.preAuthCode + } + var result PreAuthCode + app.Db.Where("app_id = ?", app.ComponentAppId).Where("expire_time >= ?", gotime.Current().Format()).Last(&result) + return result.PreAuthCode +} + +// GetComponentAccessToken 获取 access_token +func (app *App) GetComponentAccessToken() string { + if app.Db == nil { + return app.componentAccessToken + } + var result ComponentAccessToken + app.Db.Where("app_id = ?", app.ComponentAppId).Where("expire_time >= ?", gotime.Current().Format()).Last(&result) + return result.ComponentAccessToken +} + +// GetComponentVerifyTicket 获取 Ticket +func (app *App) GetComponentVerifyTicket() string { + if app.Db == nil { + return app.componentVerifyTicket + } + var result ComponentVerifyTicket + app.Db.Where("app_id = ?", app.ComponentAppId).Where("expire_time >= ?", gotime.Current().Format()).Last(&result) + return result.ComponentVerifyTicket +} diff --git a/service/wechatopen/authorizer_access_token.db.go b/service/wechatopen/authorizer_access_token.db.go new file mode 100644 index 00000000..e86078f9 --- /dev/null +++ b/service/wechatopen/authorizer_access_token.db.go @@ -0,0 +1,48 @@ +package wechatopen + +import ( + "gitee.com/dtapps/go-library/utils/gotime" + "gorm.io/gorm" + "time" +) + +// GetAuthorizerAccessTokenMonitor 获取获取/刷新接口调用令牌和监控 +func (app *App) GetAuthorizerAccessTokenMonitor() string { + // 查询 + authorizerAccessToken := app.GetAuthorizerAccessToken() + if authorizerAccessToken != "" { + return authorizerAccessToken + } + // 重新获取 + return app.SetAuthorizerAccessToken(app.CgiBinComponentApiAuthorizerToken()).AuthorizerAccessToken +} + +// SetAuthorizerAccessToken 设置获取/刷新接口调用令牌和自动获取 +func (app *App) SetAuthorizerAccessToken(info *CgiBinComponentApiAuthorizerTokenResult) CgiBinComponentApiAuthorizerTokenResponse { + if app.Db == nil || info.Result.AuthorizerAccessToken == "" || info.Result.AuthorizerRefreshToken == "" || info.authorizerAppid == "" { + return CgiBinComponentApiAuthorizerTokenResponse{} + } + app.Db.Create(&AuthorizerAccessToken{ + ComponentAppId: app.ComponentAppId, + AuthorizerAppId: info.authorizerAppid, + AuthorizerAccessToken: info.Result.AuthorizerAccessToken, + AuthorizerRefreshToken: info.Result.AuthorizerRefreshToken, + ExpiresIn: info.Result.ExpiresIn, + ExpireTime: gotime.Current().AfterHour(2).Time, + }) + return info.Result +} + +type AuthorizerAccessToken struct { + gorm.Model + ComponentAppId string `json:"component_app_id"` // 第三方平台 appid + AuthorizerAppId string `json:"authorizer_app_id"` // 授权方 appid + AuthorizerAccessToken string `json:"authorizer_access_token"` // 接口调用令牌(在授权的公众号/小程序具备 API 权限时,才有此返回值) + AuthorizerRefreshToken string `json:"authorizer_refresh_token"` // 刷新令牌(在授权的公众号具备API权限时,才有此返回值),刷新令牌主要用于第三方平台获取和刷新已授权用户的 authorizer_access_token。一旦丢失,只能让用户重新授权,才能再次拿到新的刷新令牌。用户重新授权后,之前的刷新令牌会失效 + ExpiresIn int64 `json:"expires_in"` // 有效期,单位:秒 + ExpireTime time.Time `json:"expire_time"` // 过期时间 +} + +func (m *AuthorizerAccessToken) TableName() string { + return "authorizer_access_token" +} diff --git a/service/wechatopen/cgi-bin.account.getaccountbasicinfo.go b/service/wechatopen/cgi-bin.account.getaccountbasicinfo.go new file mode 100644 index 00000000..88e3bfe5 --- /dev/null +++ b/service/wechatopen/cgi-bin.account.getaccountbasicinfo.go @@ -0,0 +1,64 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type CgiBinAccountGetAccountBasicInfoResponse struct { + Errcode int `json:"errcode"` // 返回码 + Errmsg string `json:"errmsg"` // 错误信息 + Appid string `json:"appid"` // 帐号 appid + AccountType int `json:"account_type"` // 帐号类型(1:订阅号,2:服务号,3:小程序) + PrincipalType int `json:"principal_type"` // 主体类型 + PrincipalName string `json:"principal_name"` // 主体名称 + Credential string `json:"credential"` // 主体标识 + RealnameStatus int `json:"realname_status"` // 实名验证状态 1=实名验证成功 2=实名验证中 3=实名验证失败 + WxVerifyInfo struct { + QualificationVerify bool `json:"qualification_verify"` // 是否资质认证,若是,拥有微信认证相关的权限 + NamingVerify bool `json:"naming_verify"` // 是否名称认证 + AnnualReview bool `json:"annual_review"` // 是否需要年审(qualification_verify == true 时才有该字段) + AnnualReviewBeginTime int `json:"annual_review_begin_time"` // 年审开始时间,时间戳(qualification_verify == true 时才有该字段) + AnnualReviewEndTime int `json:"annual_review_end_time"` // 年审截止时间,时间戳(qualification_verify == true 时才有该字段) + } `json:"wx_verify_info"` // 微信认证信息 + SignatureInfo struct { + Signature string `json:"signature"` // 功能介绍 + ModifyUsedCount int `json:"modify_used_count"` // 功能介绍已使用修改次数(本月) + ModifyQuota int `json:"modify_quota"` // 功能介绍修改次数总额度(本月) + } `json:"signature_info"` // 功能介绍信息 + HeadImageInfo struct { + HeadImageUrl string `json:"head_image_url"` // 头像 url + ModifyUsedCount int `json:"modify_used_count"` // 头像已使用修改次数(本年) + ModifyQuota int `json:"modify_quota"` // 头像修改次数总额度(本年) + } `json:"head_image_info"` // 头像信息 + NicknameInfo struct { + Nickname string `json:"nickname"` // 小程序名称 + ModifyUsedCount int `json:"modify_used_count"` // 小程序名称已使用修改次数(本年) + ModifyQuota int `json:"modify_quota"` // 小程序名称修改次数总额度(本年) + } `json:"nickname_info"` // 名称信息 + RegisteredCountry int `json:"registered_country"` // 注册国家 + Nickname string `json:"nickname"` // 小程序名称 +} + +type CgiBinAccountGetAccountBasicInfoResult struct { + Result CgiBinAccountGetAccountBasicInfoResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewCgiBinAccountGetAccountBasicInfoResult(result CgiBinAccountGetAccountBasicInfoResponse, body []byte, err error) *CgiBinAccountGetAccountBasicInfoResult { + return &CgiBinAccountGetAccountBasicInfoResult{Result: result, Body: body, Err: err} +} + +// CgiBinAccountGetAccountBasicInfo 获取基本信息 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Mini_Program_Basic_Info/Mini_Program_Information_Settings.html +func (app *App) CgiBinAccountGetAccountBasicInfo() *CgiBinAccountGetAccountBasicInfoResult { + app.authorizerAccessToken = app.GetAuthorizerAccessToken() + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/account/getaccountbasicinfo?access_token=%v", app.authorizerAccessToken), map[string]interface{}{}, http.MethodGet) + // 定义 + var response CgiBinAccountGetAccountBasicInfoResponse + err = json.Unmarshal(body, &response) + return NewCgiBinAccountGetAccountBasicInfoResult(response, body, err) +} diff --git a/service/wechatopen/cgi-bin.component.api_authorizer_token.go b/service/wechatopen/cgi-bin.component.api_authorizer_token.go new file mode 100644 index 00000000..6ae18f25 --- /dev/null +++ b/service/wechatopen/cgi-bin.component.api_authorizer_token.go @@ -0,0 +1,42 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type CgiBinComponentApiAuthorizerTokenResponse struct { + AuthorizerAccessToken string `json:"authorizer_access_token"` + ExpiresIn int64 `json:"expires_in"` + AuthorizerRefreshToken string `json:"authorizer_refresh_token"` +} + +type CgiBinComponentApiAuthorizerTokenResult struct { + Result CgiBinComponentApiAuthorizerTokenResponse // 结果 + Body []byte // 内容 + Err error // 错误 + authorizerAppid string // 授权方 appid +} + +func NewCgiBinComponentApiAuthorizerTokenResult(result CgiBinComponentApiAuthorizerTokenResponse, body []byte, err error, authorizerAppid string) *CgiBinComponentApiAuthorizerTokenResult { + return &CgiBinComponentApiAuthorizerTokenResult{Result: result, Body: body, Err: err, authorizerAppid: authorizerAppid} +} + +// CgiBinComponentApiAuthorizerToken 获取/刷新接口调用令牌 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/api_authorizer_token.html +func (app *App) CgiBinComponentApiAuthorizerToken() *CgiBinComponentApiAuthorizerTokenResult { + app.componentAccessToken = app.GetComponentAccessToken() + // 参数 + param := NewParams() + param["component_appid"] = app.ComponentAppId // 第三方平台 appid + param["authorizer_appid"] = app.AuthorizerAppid // 授权方 appid + param["authorizer_refresh_token"] = app.GetAuthorizerRefreshToken() // 授权码, 会在授权成功时返回给第三方平台 + params := app.NewParamsWith(param) + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=%v", app.componentAccessToken), params, http.MethodPost) + // 定义 + var response CgiBinComponentApiAuthorizerTokenResponse + err = json.Unmarshal(body, &response) + return NewCgiBinComponentApiAuthorizerTokenResult(response, body, err, app.AuthorizerAppid) +} diff --git a/service/wechatopen/cgi-bin.component.api_component_token.go b/service/wechatopen/cgi-bin.component.api_component_token.go new file mode 100644 index 00000000..c1a31b57 --- /dev/null +++ b/service/wechatopen/cgi-bin.component.api_component_token.go @@ -0,0 +1,39 @@ +package wechatopen + +import ( + "encoding/json" + "net/http" +) + +type CgiBinComponentApiComponentTokenResponse struct { + ComponentAccessToken string `json:"component_access_token"` // 第三方平台 access_token + ExpiresIn int64 `json:"expires_in"` // 有效期,单位:秒 +} + +type CgiBinComponentApiComponentTokenResult struct { + Result CgiBinComponentApiComponentTokenResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewCgiBinComponentApiComponentTokenResult(result CgiBinComponentApiComponentTokenResponse, body []byte, err error) *CgiBinComponentApiComponentTokenResult { + return &CgiBinComponentApiComponentTokenResult{Result: result, Body: body, Err: err} +} + +// CgiBinComponentApiComponentToken 令牌 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/component_access_token.html +func (app *App) CgiBinComponentApiComponentToken() *CgiBinComponentApiComponentTokenResult { + app.componentVerifyTicket = app.GetComponentVerifyTicket() + // 参数 + param := NewParams() + param["component_appid"] = app.ComponentAppId // 第三方平台 appid + param["component_appsecret"] = app.ComponentAppSecret // 第三方平台 appsecret + param["component_verify_ticket"] = app.componentVerifyTicket // 微信后台推送的 ticket + params := app.NewParamsWith(param) + // 请求 + body, err := app.request("https://api.weixin.qq.com/cgi-bin/component/api_component_token", params, http.MethodPost) + // 定义 + var response CgiBinComponentApiComponentTokenResponse + err = json.Unmarshal(body, &response) + return NewCgiBinComponentApiComponentTokenResult(response, body, err) +} diff --git a/service/wechatopen/cgi-bin.component.api_create_preauthcode.go b/service/wechatopen/cgi-bin.component.api_create_preauthcode.go new file mode 100644 index 00000000..695c35d9 --- /dev/null +++ b/service/wechatopen/cgi-bin.component.api_create_preauthcode.go @@ -0,0 +1,38 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type CgiBinComponentApiCreatePreAuthCodenResponse struct { + PreAuthCode string `json:"pre_auth_code"` // 预授权码 + ExpiresIn int64 `json:"expires_in"` // 有效期,单位:秒 +} + +type CgiBinComponentApiCreatePreAuthCodenResult struct { + Result CgiBinComponentApiCreatePreAuthCodenResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewCgiBinComponentApiCreatePreAuthCodenResult(result CgiBinComponentApiCreatePreAuthCodenResponse, body []byte, err error) *CgiBinComponentApiCreatePreAuthCodenResult { + return &CgiBinComponentApiCreatePreAuthCodenResult{Result: result, Body: body, Err: err} +} + +// CgiBinComponentApiCreatePreAuthCoden 预授权码 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/pre_auth_code.html +func (app *App) CgiBinComponentApiCreatePreAuthCoden() *CgiBinComponentApiCreatePreAuthCodenResult { + app.componentAccessToken = app.GetComponentAccessToken() + // 参数 + param := NewParams() + param["component_appid"] = app.ComponentAppId // 第三方平台 appid + params := app.NewParamsWith(param) + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=%v", app.componentAccessToken), params, http.MethodPost) + // 定义 + var response CgiBinComponentApiCreatePreAuthCodenResponse + err = json.Unmarshal(body, &response) + return NewCgiBinComponentApiCreatePreAuthCodenResult(response, body, err) +} diff --git a/service/wechatopen/cgi-bin.component.api_get_authorizer_info.go b/service/wechatopen/cgi-bin.component.api_get_authorizer_info.go new file mode 100644 index 00000000..69e52fc4 --- /dev/null +++ b/service/wechatopen/cgi-bin.component.api_get_authorizer_info.go @@ -0,0 +1,97 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type CgiBinComponentApiGetAuthorizerInfoResponse struct { + AuthorizerInfo struct { + NickName string `json:"nick_name"` // 昵称 + HeadImg string `json:"head_img"` // 头像 + ServiceTypeInfo struct { + Id int `json:"id"` // 0=普通小程序 2=门店小程序 3=门店小程序 4=小游戏 10=小商店 12=试用小程序 + } `json:"service_type_info"` // 小程序类型 + VerifyTypeInfo struct { + Id int `json:"id"` // -1=未认证 0=微信认证 + } `json:"verify_type_info"` // 小程序认证类型 + UserName string `json:"user_name"` // 原始 ID + PrincipalName string `json:"principal_name"` // 主体名称 + Signature string `json:"signature"` // 帐号介绍 + BusinessInfo struct { + OpenPay int `json:"open_pay"` + OpenShake int `json:"open_shake"` + OpenScan int `json:"open_scan"` + OpenCard int `json:"open_card"` + OpenStore int `json:"open_store"` + } `json:"business_info"` // 用以了解功能的开通状况(0代表未开通,1代表已开通) + QrcodeUrl string `json:"qrcode_url"` // 二维码图片的 URL,开发者最好自行也进行保存 + MiniProgramInfo struct { + Network struct { + RequestDomain []string `json:"RequestDomain"` + WsRequestDomain []string `json:"WsRequestDomain"` + UploadDomain []string `json:"UploadDomain"` + DownloadDomain []string `json:"DownloadDomain"` + BizDomain []string `json:"BizDomain"` + UDPDomain []string `json:"UDPDomain"` + TCPDomain []interface{} `json:"TCPDomain"` + NewRequestDomain []interface{} `json:"NewRequestDomain"` + NewWsRequestDomain []interface{} `json:"NewWsRequestDomain"` + NewUploadDomain []interface{} `json:"NewUploadDomain"` + NewDownloadDomain []interface{} `json:"NewDownloadDomain"` + NewBizDomain []interface{} `json:"NewBizDomain"` + NewUDPDomain []interface{} `json:"NewUDPDomain"` + NewTCPDomain []interface{} `json:"NewTCPDomain"` + } `json:"network"` // 小程序配置的合法域名信息 + Categories []struct { + First string `json:"first"` + Second string `json:"second"` + } `json:"categories"` // 小程序配置的类目信息 + VisitStatus int `json:"visit_status"` + } `json:"MiniProgramInfo"` // 小程序配置,根据这个字段判断是否为小程序类型授权 + Alias string `json:"alias"` // 公众号所设置的微信号,可能为空 + Idc int `json:"idc"` + } `json:"authorizer_info"` // 小程序帐号信息 + AuthorizationInfo struct { + AuthorizerAppid string `json:"authorizer_appid"` // 授权方 appid + FuncInfo []struct { + FuncscopeCategory struct { + Id int `json:"id"` + } `json:"funcscope_category"` + ConfirmInfo struct { + NeedConfirm int `json:"need_confirm"` + AlreadyConfirm int `json:"already_confirm"` + CanConfirm int `json:"can_confirm"` + } `json:"confirm_info,omitempty"` + } `json:"func_info"` // 授权给开发者的权限集列表 + AuthorizerRefreshToken string `json:"authorizer_refresh_token"` + } `json:"authorization_info"` // 授权信息 +} + +type CgiBinComponentApiGetAuthorizerInfoResult struct { + Result CgiBinComponentApiGetAuthorizerInfoResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewCgiBinComponentApiGetAuthorizerInfoResult(result CgiBinComponentApiGetAuthorizerInfoResponse, body []byte, err error) *CgiBinComponentApiGetAuthorizerInfoResult { + return &CgiBinComponentApiGetAuthorizerInfoResult{Result: result, Body: body, Err: err} +} + +// CgiBinComponentApiGetAuthorizerInfo 获取授权帐号详情 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/api_get_authorizer_info.html +func (app *App) CgiBinComponentApiGetAuthorizerInfo() *CgiBinComponentApiGetAuthorizerInfoResult { + app.componentAccessToken = app.GetComponentAccessToken() + // 参数 + param := NewParams() + param["component_appid"] = app.ComponentAppId // 第三方平台 appid + param["authorizer_appid"] = app.AuthorizerAppid // 授权方 appid + params := app.NewParamsWith(param) + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%v", app.componentAccessToken), params, http.MethodPost) + // 定义 + var response CgiBinComponentApiGetAuthorizerInfoResponse + err = json.Unmarshal(body, &response) + return NewCgiBinComponentApiGetAuthorizerInfoResult(response, body, err) +} diff --git a/service/wechatopen/cgi-bin.component.api_query_auth.go b/service/wechatopen/cgi-bin.component.api_query_auth.go new file mode 100644 index 00000000..df2e26bf --- /dev/null +++ b/service/wechatopen/cgi-bin.component.api_query_auth.go @@ -0,0 +1,53 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type CgiBinComponentApiQueryAuthResponse struct { + AuthorizationInfo struct { + AuthorizerAppid string `json:"authorizer_appid"` // 授权方 appid + AuthorizerAccessToken string `json:"authorizer_access_token"` // 接口调用令牌(在授权的公众号/小程序具备 API 权限时,才有此返回值) + ExpiresIn int64 `json:"expires_in"` // authorizer_access_token 的有效期(在授权的公众号/小程序具备API权限时,才有此返回值),单位:秒 + AuthorizerRefreshToken string `json:"authorizer_refresh_token"` // 刷新令牌(在授权的公众号具备API权限时,才有此返回值),刷新令牌主要用于第三方平台获取和刷新已授权用户的 authorizer_access_token。一旦丢失,只能让用户重新授权,才能再次拿到新的刷新令牌。用户重新授权后,之前的刷新令牌会失效 + FuncInfo []struct { + FuncscopeCategory struct { + Id int `json:"id"` + } `json:"funcscope_category"` + ConfirmInfo struct { + NeedConfirm int `json:"need_confirm"` + AlreadyConfirm int `json:"already_confirm"` + CanConfirm int `json:"can_confirm"` + } `json:"confirm_info,omitempty"` + } `json:"func_info"` + } `json:"authorization_info"` +} + +type CgiBinComponentApiQueryAuthResult struct { + Result CgiBinComponentApiQueryAuthResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewCgiBinComponentApiQueryAuthResult(result CgiBinComponentApiQueryAuthResponse, body []byte, err error) *CgiBinComponentApiQueryAuthResult { + return &CgiBinComponentApiQueryAuthResult{Result: result, Body: body, Err: err} +} + +// CgiBinComponentApiQueryAuth 使用授权码获取授权信息 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/authorization_info.html +func (app *App) CgiBinComponentApiQueryAuth(authorizationCode string) *CgiBinComponentApiQueryAuthResult { + app.componentAccessToken = app.GetComponentAccessToken() + // 参数 + param := NewParams() + param["component_appid"] = app.ComponentAppId // 第三方平台 appid + param["authorization_code"] = authorizationCode // 授权码, 会在授权成功时返回给第三方平台 + params := app.NewParamsWith(param) + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=%v", app.componentAccessToken), params, http.MethodPost) + // 定义 + var response CgiBinComponentApiQueryAuthResponse + err = json.Unmarshal(body, &response) + return NewCgiBinComponentApiQueryAuthResult(response, body, err) +} diff --git a/service/wechatopen/cgi-bin.component.api_start_push_ticket.go b/service/wechatopen/cgi-bin.component.api_start_push_ticket.go new file mode 100644 index 00000000..82a0b6d2 --- /dev/null +++ b/service/wechatopen/cgi-bin.component.api_start_push_ticket.go @@ -0,0 +1,39 @@ +package wechatopen + +import ( + "encoding/json" + "net/http" +) + +type CgiBinComponentApiStartPushTicketResponse struct { + AccessToken string `json:"access_token"` // 获取到的凭证 + ExpiresIn int `json:"expires_in"` // 凭证有效时间,单位:秒。目前是7200秒之内的值 + Errcode int `json:"errcode"` // 错误码 + Errmsg string `json:"errmsg"` // 错误信息 +} + +type CgiBinComponentApiStartPushTicketResult struct { + Result CgiBinComponentApiStartPushTicketResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewCgiBinComponentApiStartPushTicketResult(result CgiBinComponentApiStartPushTicketResponse, body []byte, err error) *CgiBinComponentApiStartPushTicketResult { + return &CgiBinComponentApiStartPushTicketResult{Result: result, Body: body, Err: err} +} + +// CgiBinComponentApiStartPushTicket 启动ticket推送服务 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/component_verify_ticket_service.html +func (app *App) CgiBinComponentApiStartPushTicket() *CgiBinComponentApiStartPushTicketResult { + // 参数 + param := NewParams() + param["component_appid"] = app.ComponentAppId // 平台型第三方平台的appid + param["component_secret"] = app.ComponentAppSecret // 平台型第三方平台的APPSECRET + params := app.NewParamsWith(param) + // 请求 + body, err := app.request("https://api.weixin.qq.com/cgi-bin/component/api_start_push_ticket", params, http.MethodPost) + // 定义 + var response CgiBinComponentApiStartPushTicketResponse + err = json.Unmarshal(body, &response) + return NewCgiBinComponentApiStartPushTicketResult(response, body, err) +} diff --git a/service/wechatopen/cgi-bin.get_api_domain_ip.go b/service/wechatopen/cgi-bin.get_api_domain_ip.go new file mode 100644 index 00000000..46ad3445 --- /dev/null +++ b/service/wechatopen/cgi-bin.get_api_domain_ip.go @@ -0,0 +1,32 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type GetCallBackIpResponse struct { + IpList []string `json:"ip_list"` +} + +type GetCallBackIpResult struct { + Result GetCallBackIpResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewGetCallBackIpResult(result GetCallBackIpResponse, body []byte, err error) *GetCallBackIpResult { + return &GetCallBackIpResult{Result: result, Body: body, Err: err} +} + +// CgiBinGetApiDomainIp 获取微信服务器IP地址 +// https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_the_WeChat_server_IP_address.html +func (app *App) CgiBinGetApiDomainIp(componentAccessToken string) *GetCallBackIpResult { + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/get_api_domain_ip?access_token=%s", componentAccessToken), map[string]interface{}{}, http.MethodGet) + // 定义 + var response GetCallBackIpResponse + err = json.Unmarshal(body, &response) + return NewGetCallBackIpResult(response, body, err) +} diff --git a/service/wechatopen/component_access_token.db.go b/service/wechatopen/component_access_token.db.go new file mode 100644 index 00000000..678a4f24 --- /dev/null +++ b/service/wechatopen/component_access_token.db.go @@ -0,0 +1,46 @@ +package wechatopen + +import ( + "gitee.com/dtapps/go-library/utils/gotime" + "gorm.io/gorm" + "time" +) + +// GetComponentAccessTokenMonitor 获取令牌和监控 +func (app *App) GetComponentAccessTokenMonitor() string { + // 查询 + componentAccessToken := app.GetComponentAccessToken() + // 判断 + result := app.CgiBinGetApiDomainIp(componentAccessToken) + if len(result.Result.IpList) > 0 { + return componentAccessToken + } + // 重新获取 + return app.SetComponentAccessToken(app.CgiBinComponentApiComponentToken()) +} + +// SetComponentAccessToken 设置令牌 +func (app *App) SetComponentAccessToken(info *CgiBinComponentApiComponentTokenResult) string { + if app.Db == nil || info.Result.ComponentAccessToken == "" { + return "" + } + app.Db.Create(&ComponentAccessToken{ + AppId: app.ComponentAppId, + ComponentAccessToken: info.Result.ComponentAccessToken, + ExpiresIn: info.Result.ExpiresIn, + ExpireTime: gotime.Current().AfterSeconds(7200).Time, + }) + return info.Result.ComponentAccessToken +} + +type ComponentAccessToken struct { + gorm.Model + AppId string `json:"app_id"` // 第三方平台 appid + ComponentAccessToken string `json:"component_access_token"` // 第三方平台 access_token + ExpiresIn int64 `json:"expires_in"` // 有效期,单位:秒 + ExpireTime time.Time `json:"expire_time"` // 过期时间 +} + +func (m *ComponentAccessToken) TableName() string { + return "component_access_token" +} diff --git a/service/wechatopen/component_verify_ticket.db.go b/service/wechatopen/component_verify_ticket.db.go new file mode 100644 index 00000000..4de0fa6e --- /dev/null +++ b/service/wechatopen/component_verify_ticket.db.go @@ -0,0 +1,35 @@ +package wechatopen + +import ( + "gitee.com/dtapps/go-library/utils/gotime" + "gorm.io/gorm" + "time" +) + +// SetComponentVerifyTicket 设置微信后台推送的ticket +func (app *App) SetComponentVerifyTicket(info *ResponseServeHttpVerifyTicket) string { + if info.ComponentVerifyTicket == "" { + return "" + } + app.Db.Create(&ComponentVerifyTicket{ + AppId: info.AppId, + CreateTime: info.CreateTime, + InfoType: info.InfoType, + ComponentVerifyTicket: info.ComponentVerifyTicket, + ExpireTime: gotime.Current().AfterHour(12).Time, + }) + return info.ComponentVerifyTicket +} + +type ComponentVerifyTicket struct { + gorm.Model + AppId string `json:"app_id"` // 第三方平台 appid + CreateTime int64 `json:"create_time"` // 时间戳,单位:s + InfoType string `json:"info_type"` // 固定为:"component_verify_ticket" + ComponentVerifyTicket string `json:"component_verify_ticket"` // Ticket 内容 + ExpireTime time.Time `json:"expire_time"` // 过期时间 +} + +func (m *ComponentVerifyTicket) TableName() string { + return "component_verify_ticket" +} diff --git a/service/wechatopen/mongodb.go b/service/wechatopen/mongodb.go new file mode 100644 index 00000000..0cae00fe --- /dev/null +++ b/service/wechatopen/mongodb.go @@ -0,0 +1,47 @@ +package wechatopen + +import ( + "encoding/json" + "gitee.com/dtapps/go-library/utils/gohttp" + "gitee.com/dtapps/go-library/utils/gotime" +) + +// 日志 +type mongoZap struct { + Url string `json:"url" bson:"url"` + Params interface{} `json:"params" bson:"params"` + Method string `json:"method" bson:"method"` + Header interface{} `json:"header" bson:"header"` + Status string `json:"status" bson:"status"` + StatusCode int `json:"status_code" bson:"status_code"` + Body interface{} `json:"body" bson:"body"` + ContentLength int64 `json:"content_length" bson:"content_length"` + CreateTime string `json:"create_time" bson:"create_time"` +} + +func (m *mongoZap) Database() string { + return "zap_logs" +} + +func (m *mongoZap) TableName() string { + return "wechatopen_" + gotime.Current().SetFormat("200601") +} + +func (app *App) mongoLog(url string, params map[string]interface{}, method string, request gohttp.Response) { + if app.Mongo.Db == nil { + return + } + var body map[string]interface{} + _ = json.Unmarshal(request.Body, &body) + app.Mongo.Model(&mongoZap{}).InsertOne(mongoZap{ + Url: url, + Params: params, + Method: method, + Header: request.Header, + Status: request.Status, + StatusCode: request.StatusCode, + Body: body, + ContentLength: request.ContentLength, + CreateTime: gotime.Current().Format(), + }) +} diff --git a/service/wechatopen/params.go b/service/wechatopen/params.go new file mode 100644 index 00000000..b404f315 --- /dev/null +++ b/service/wechatopen/params.go @@ -0,0 +1,27 @@ +package wechatopen + +// Params 请求参数 +type Params map[string]interface{} + +func NewParams() Params { + p := make(Params) + return p +} + +func (app *App) NewParamsWith(params ...Params) Params { + p := make(Params) + for _, v := range params { + p.SetParams(v) + } + return p +} + +func (p Params) Set(key string, value interface{}) { + p[key] = value +} + +func (p Params) SetParams(params Params) { + for key, value := range params { + p[key] = value + } +} diff --git a/service/wechatopen/pre_auth_code.db.go b/service/wechatopen/pre_auth_code.db.go new file mode 100644 index 00000000..de82512c --- /dev/null +++ b/service/wechatopen/pre_auth_code.db.go @@ -0,0 +1,49 @@ +package wechatopen + +import ( + "gitee.com/dtapps/go-library/utils/gotime" + "gorm.io/gorm" + "time" +) + +// GetPreAuthCodeMonitor 获取预授权码和监控 +func (app *App) GetPreAuthCodeMonitor() string { + // 查询 + preAuthCode := app.GetPreAuthCode() + if preAuthCode != "" { + return preAuthCode + } + // 重新获取 + return app.SetPreAuthCode(app.CgiBinComponentApiCreatePreAuthCoden()) +} + +// SetPreAuthCode 设置预授权码和自动获取 +func (app *App) SetPreAuthCode(info *CgiBinComponentApiCreatePreAuthCodenResult) string { + if app.Db == nil || info.Result.PreAuthCode == "" { + return "" + } + app.Db.Create(&PreAuthCode{ + AppId: app.ComponentAppId, + PreAuthCode: info.Result.PreAuthCode, + ExpiresIn: info.Result.ExpiresIn, + ExpireTime: gotime.Current().AfterSeconds(1700).Time, + }) + return info.Result.PreAuthCode +} + +type PreAuthCode struct { + gorm.Model + AppId string `json:"app_id"` // 第三方平台 appid + PreAuthCode string `json:"pre_auth_code"` // 预授权码 + ExpiresIn int64 `json:"expires_in"` // 有效期,单位:秒 + ExpireTime time.Time `json:"expire_time"` // 过期时间 +} + +func (m *PreAuthCode) TableName() string { + return "pre_auth_code" +} + +// PreAuthCodeDelete 删除过期或使用过的预授权码 +func (app *App) PreAuthCodeDelete(id uint) int64 { + return app.Db.Where("id = ?", id).Delete(&PreAuthCode{}).RowsAffected +} diff --git a/service/wechatopen/service_http.authorizer_appid.go b/service/wechatopen/service_http.authorizer_appid.go new file mode 100644 index 00000000..31ac3d65 --- /dev/null +++ b/service/wechatopen/service_http.authorizer_appid.go @@ -0,0 +1,60 @@ +package wechatopen + +import ( + "errors" + "net/http" + "strconv" +) + +// ServeHttpAuthorizerAppid 授权跳转 +func (app *App) ServeHttpAuthorizerAppid(r *http.Request) (resp CgiBinComponentApiQueryAuthResponse, agentUserId int64, pacId uint, err error) { + var ( + query = r.URL.Query() + + authCode = query.Get("auth_code") + expiresIn = query.Get("expires_in") + ) + + agentUserId = ToInt64(query.Get("agent_user_id")) + + pacId = ToUint(query.Get("pac_id")) + + if authCode == "" { + return resp, agentUserId, pacId, errors.New("找不到授权码参数") + } + + if expiresIn == "" { + return resp, agentUserId, pacId, errors.New("找不到过期时间参数") + } + + info := app.CgiBinComponentApiQueryAuth(authCode) + if info.Result.AuthorizationInfo.AuthorizerAppid == "" { + return resp, agentUserId, pacId, errors.New("获取失败") + } + + return info.Result, agentUserId, pacId, nil +} + +// ToFloat64 string到float64 +func ToFloat64(s string) float64 { + i, _ := strconv.ParseFloat(s, 64) + return i +} + +// ToInt64 string到int64 +func ToInt64(s string) int64 { + i, err := strconv.ParseInt(s, 10, 64) + if err == nil { + return i + } + return int64(ToFloat64(s)) +} + +// ToUint string到uint64 +func ToUint(s string) uint { + i, err := strconv.ParseUint(s, 10, 64) + if err == nil { + return uint(i) + } + return 0 +} diff --git a/service/wechatopen/service_http.verify_ticket.go b/service/wechatopen/service_http.verify_ticket.go new file mode 100644 index 00000000..b515e615 --- /dev/null +++ b/service/wechatopen/service_http.verify_ticket.go @@ -0,0 +1,131 @@ +package wechatopen + +import ( + "encoding/base64" + "encoding/xml" + "errors" + "fmt" + "github.com/mitchellh/mapstructure" + "io/ioutil" + "net/http" + "strings" +) + +// ResponseServeHttpVerifyTicket 验证票据推送 +type ResponseServeHttpVerifyTicket struct { + XMLName xml.Name + AppId string `xml:"AppId" json:"AppId"` // 第三方平台 appid + CreateTime int64 `xml:"CreateTime" json:"CreateTime"` // 时间戳,单位:s + InfoType string `xml:"InfoType" json:"InfoType"` // 固定为:"component_verify_ticket" + ComponentVerifyTicket string `xml:"ComponentVerifyTicket" json:"ComponentVerifyTicket"` // Ticket 内容 +} + +type cipherRequestHttpBody struct { + AppId string `xml:"AppId" json:"AppId"` // 第三方平台 appid + Encrypt string `xml:"Encrypt" json:"Encrypt"` // 加密内容 +} + +// ServeHttpVerifyTicket 验证票据推送 +func (app *App) ServeHttpVerifyTicket(r *http.Request) (resp *ResponseServeHttpVerifyTicket, err error) { + var ( + query = r.URL.Query() + + wantSignature string + haveSignature = query.Get("signature") + timestamp = query.Get("timestamp") + nonce = query.Get("nonce") + + // post + haveMsgSignature = query.Get("msg_signature") + encryptType = query.Get("encrypt_type") + + // handle vars + data []byte + requestHttpBody = &cipherRequestHttpBody{} + ) + + if haveSignature == "" { + err = errors.New("找不到签名参数") + return + } + + if timestamp == "" { + return resp, errors.New("找不到时间戳参数") + } + + if nonce == "" { + return resp, errors.New("未找到随机数参数") + } + + wantSignature = Sign(app.MessageToken, timestamp, nonce) + if haveSignature != wantSignature { + return resp, errors.New("签名错误") + } + + // 进入事件执行 + if encryptType != "aes" { + err = errors.New("未知的加密类型: " + encryptType) + return + } + if haveMsgSignature == "" { + err = errors.New("找不到签名参数") + return + } + + data, err = ioutil.ReadAll(r.Body) + if err != nil { + return resp, err + } + + xmlDecode := XmlDecode(string(data)) + if len(xmlDecode) <= 0 { + return resp, errors.New(fmt.Sprintf("Xml解码错误:%s", xmlDecode)) + } + + err = mapstructure.Decode(xmlDecode, &requestHttpBody) + if err != nil { + return resp, errors.New(fmt.Sprintf("mapstructure 解码错误:%s", xmlDecode)) + } + + if requestHttpBody.Encrypt == "" { + return resp, errors.New(fmt.Sprintf("未找到加密数据:%s", requestHttpBody)) + } + + cipherData, err := base64.StdEncoding.DecodeString(requestHttpBody.Encrypt) + if err != nil { + return resp, errors.New(fmt.Sprintf("Encrypt 解码字符串错误:%v", err)) + } + + AesKey, err := base64.StdEncoding.DecodeString(app.MessageKey + "=") + if err != nil { + return resp, errors.New(fmt.Sprintf("MessageKey 解码字符串错误:%v", err)) + } + + msg, err := AesDecrypt(cipherData, AesKey) + if err != nil { + return resp, errors.New(fmt.Sprintf("AES解密错误:%v", err)) + } + + str := string(msg) + + left := strings.Index(str, "") + if left <= 0 { + return resp, errors.New(fmt.Sprintf("匹配不到:%v", left)) + } + right := strings.Index(str, "") + if right <= 0 { + return resp, errors.New(fmt.Sprintf("匹配不到:%v", right)) + } + msgStr := str[left:right] + if len(msgStr) == 0 { + return resp, errors.New(fmt.Sprintf("提取错误:%v", msgStr)) + } + + resp = &ResponseServeHttpVerifyTicket{} + err = xml.Unmarshal([]byte(msgStr+""), resp) + if err != nil { + return resp, errors.New(fmt.Sprintf("解析错误:%v", err)) + } + + return resp, nil +} diff --git a/service/wechatopen/sign.go b/service/wechatopen/sign.go new file mode 100644 index 00000000..a1577029 --- /dev/null +++ b/service/wechatopen/sign.go @@ -0,0 +1,87 @@ +package wechatopen + +import ( + "bufio" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha1" + "encoding/hex" + "errors" + "io" + "sort" +) + +// Sign 微信公众号 url 签名. +func Sign(token, timestamp, nonce string) (signature string) { + strs := sort.StringSlice{token, timestamp, nonce} + strs.Sort() + + buf := make([]byte, 0, len(token)+len(timestamp)+len(nonce)) + buf = append(buf, strs[0]...) + buf = append(buf, strs[1]...) + buf = append(buf, strs[2]...) + + hashsum := sha1.Sum(buf) + return hex.EncodeToString(hashsum[:]) +} + +// MsgSign 微信公众号/企业号 消息体签名. +func MsgSign(token, timestamp, nonce, encryptedMsg string) (signature string) { + strs := sort.StringSlice{token, timestamp, nonce, encryptedMsg} + strs.Sort() + + h := sha1.New() + + bufw := bufio.NewWriterSize(h, 128) // sha1.BlockSize 的整数倍 + bufw.WriteString(strs[0]) + bufw.WriteString(strs[1]) + bufw.WriteString(strs[2]) + bufw.WriteString(strs[3]) + bufw.Flush() + + hashsum := h.Sum(nil) + return hex.EncodeToString(hashsum) +} + +// CheckSignature 微信公众号签名检查 +func CheckSignature(signature, timeStamp, nonce string, token string) bool { + paramsArray := []string{token, timeStamp, nonce} + // 字典序排序 + sort.Strings(paramsArray) + paramsMsg := "" + for _, value := range paramsArray { + //fmt.Println(value) + paramsMsg += value + } + //sha1 + sha1Param := sha1.New() + sha1Param.Write([]byte(paramsMsg)) + msg := hex.EncodeToString(sha1Param.Sum([]byte(""))) + return msg == signature +} + +func AesDecrypt(cipherData []byte, aesKey []byte) ([]byte, error) { + k := len(aesKey) //PKCS#7 + + if len(cipherData)%k != 0 { + return nil, errors.New("crypto/cipher: 密文大小不是aes密钥长度的倍数") + } + + // 创建加密算法实例 + block, err := aes.NewCipher(aesKey) + if err != nil { + return nil, err + } + + iv := make([]byte, aes.BlockSize) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + // 创建加密客户端实例 + blockMode := cipher.NewCBCDecrypter(block, iv) + plainData := make([]byte, len(cipherData)) + blockMode.CryptBlocks(plainData, cipherData) + + return plainData, nil +} diff --git a/service/wechatopen/sns.component.jscode2session.go b/service/wechatopen/sns.component.jscode2session.go new file mode 100644 index 00000000..b701ba74 --- /dev/null +++ b/service/wechatopen/sns.component.jscode2session.go @@ -0,0 +1,131 @@ +package wechatopen + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/json" + "errors" + "net/http" + "strings" +) + +type SnsComponentJsCode2sessionResponse struct { + Openid string `json:"openid"` // 用户唯一标识的 openid + SessionKey string `json:"session_key"` // 会话密钥 + Unionid string `json:"unionid"` // 用户在开放平台的唯一标识符,在满足 UnionID 下发条件的情况下会返回,详见 UnionID 机制说明。 +} + +type SnsComponentJsCode2sessionResult struct { + Result SnsComponentJsCode2sessionResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewSnsComponentJsCode2sessionResult(result SnsComponentJsCode2sessionResponse, body []byte, err error) *SnsComponentJsCode2sessionResult { + return &SnsComponentJsCode2sessionResult{Result: result, Body: body, Err: err} +} + +// SnsComponentJsCode2session 小程序登录 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/others/WeChat_login.html +func (app *App) SnsComponentJsCode2session(jsCode string) *SnsComponentJsCode2sessionResult { + app.componentAccessToken = app.GetComponentAccessToken() + // 参数 + params := NewParams() + params["appid"] = app.AuthorizerAppid // 小程序的 AppID + params["js_code"] = jsCode // wx.login 获取的 code + params["grant_type"] = "authorization_code" // 填 authorization_code + params["component_appid"] = app.ComponentAppId // 第三方平台 appid + params["component_access_token"] = app.componentAccessToken // 第三方平台的component_access_token + // 请求 + body, err := app.request("https://api.weixin.qq.com/sns/component/jscode2session", params, http.MethodGet) + // 定义 + var response SnsComponentJsCode2sessionResponse + err = json.Unmarshal(body, &response) + return NewSnsComponentJsCode2sessionResult(response, body, err) +} + +type UserInfo struct { + EncryptedData string `json:"encrypted_data"` + Iv string `json:"iv"` +} + +type UserInfoResponse struct { + OpenId string `json:"openId"` + NickName string `json:"nickName"` + Gender int `json:"gender"` + City string `json:"city"` + Province string `json:"province"` + Country string `json:"country"` + AvatarUrl string `json:"avatarUrl"` + UnionId string `json:"unionId"` + Watermark struct { + AppID string `json:"appid"` + Timestamp int64 `json:"timestamp"` + } `json:"watermark"` +} + +type UserInfoResult struct { + Result UserInfoResponse // 结果 + Err error // 错误 +} + +func NewUserInfoResult(result UserInfoResponse, err error) *UserInfoResult { + return &UserInfoResult{Result: result, Err: err} +} + +// UserInfo 解密用户信息 +func (r *SnsComponentJsCode2sessionResult) UserInfo(param UserInfo) *UserInfoResult { + var response UserInfoResponse + aesKey, err := base64.StdEncoding.DecodeString(r.Result.SessionKey) + if err != nil { + return NewUserInfoResult(response, err) + } + cipherText, err := base64.StdEncoding.DecodeString(param.EncryptedData) + if err != nil { + return NewUserInfoResult(response, err) + } + ivBytes, err := base64.StdEncoding.DecodeString(param.Iv) + if err != nil { + return NewUserInfoResult(response, err) + } + block, err := aes.NewCipher(aesKey) + if err != nil { + return NewUserInfoResult(response, err) + } + mode := cipher.NewCBCDecrypter(block, ivBytes) + mode.CryptBlocks(cipherText, cipherText) + cipherText, err = r.pkcs7Unpaid(cipherText, block.BlockSize()) + if err != nil { + return NewUserInfoResult(response, err) + } + err = json.Unmarshal(cipherText, &response) + if err != nil { + return NewUserInfoResult(response, err) + } + return NewUserInfoResult(response, err) +} + +func (u *UserInfoResponse) UserInfoAvatarUrlReal() string { + return strings.Replace(u.AvatarUrl, "/132", "/0", -1) +} + +func (r *SnsComponentJsCode2sessionResult) pkcs7Unpaid(data []byte, blockSize int) ([]byte, error) { + if blockSize <= 0 { + return nil, errors.New("invalid block size") + } + if len(data)%blockSize != 0 || len(data) == 0 { + return nil, errors.New("invalid PKCS7 data") + } + c := data[len(data)-1] + n := int(c) + if n == 0 || n > len(data) { + return nil, errors.New("invalid padding on input") + } + for i := 0; i < n; i++ { + if data[len(data)-n+i] != c { + return nil, errors.New("invalid padding on input") + } + } + return data[:len(data)-n], nil +} diff --git a/service/wechatopen/wxa.addtotemplate.go b/service/wechatopen/wxa.addtotemplate.go new file mode 100644 index 00000000..bc17b569 --- /dev/null +++ b/service/wechatopen/wxa.addtotemplate.go @@ -0,0 +1 @@ +package wechatopen diff --git a/service/wechatopen/wxa.bind_tester.go b/service/wechatopen/wxa.bind_tester.go new file mode 100644 index 00000000..453e4cab --- /dev/null +++ b/service/wechatopen/wxa.bind_tester.go @@ -0,0 +1,53 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaBindTesterResponse struct { + Errcode int `json:"errcode"` // 错误码 + Errmsg string `json:"errmsg"` // 错误信息 + Userstr string `json:"userstr"` // 人员对应的唯一字符串 +} + +type WxaBindTesterResult struct { + Result WxaBindTesterResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaBindTesterResult(result WxaBindTesterResponse, body []byte, err error) *WxaBindTesterResult { + return &WxaBindTesterResult{Result: result, Body: body, Err: err} +} + +// WxaBindTester 绑定微信用户为体验者 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Mini_Program_AdminManagement/Admin.html +func (app *App) WxaBindTester(wechatid string) *WxaBindTesterResult { + app.authorizerAccessToken = app.GetAuthorizerAccessToken() + // 参数 + params := NewParams() + params["wechatid"] = wechatid + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/bind_tester?access_token=%s", app.authorizerAccessToken), params, http.MethodPost) + // 定义 + var response WxaBindTesterResponse + err = json.Unmarshal(body, &response) + return NewWxaBindTesterResult(response, body, err) +} + +// ErrcodeInfo 错误描述 +func (resp *WxaBindTesterResult) ErrcodeInfo() string { + switch resp.Result.Errcode { + case 85001: + return "微信号不存在或微信号设置为不可搜索" + case 85002: + return "小程序绑定的体验者数量达到上限" + case 85003: + return "微信号绑定的小程序体验者达到上限" + case 85004: + return "微信号已经绑定" + } + return "系统繁忙" +} diff --git a/service/wechatopen/wxa.commit.go b/service/wechatopen/wxa.commit.go new file mode 100644 index 00000000..a3ae1478 --- /dev/null +++ b/service/wechatopen/wxa.commit.go @@ -0,0 +1,69 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaCommitResponse struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` +} + +type WxaCommitResult struct { + Result WxaCommitResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaCommitResult(result WxaCommitResponse, body []byte, err error) *WxaCommitResult { + return &WxaCommitResult{Result: result, Body: body, Err: err} +} + +// WxaCommit 上传小程序代码并生成体验版 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/code/commit.html +func (app *App) WxaCommit(notMustParams ...Params) *WxaCommitResult { + app.authorizerAccessToken = app.GetAuthorizerAccessToken() + // 参数 + params := app.NewParamsWith(notMustParams...) + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/commit?access_token=%s", app.authorizerAccessToken), params, http.MethodPost) + // 定义 + var response WxaCommitResponse + err = json.Unmarshal(body, &response) + return NewWxaCommitResult(response, body, err) +} + +// ErrcodeInfo 错误描述 +func (resp *WxaCommitResult) ErrcodeInfo() string { + switch resp.Result.Errcode { + case 85013: + return "无效的自定义配置" + case 85014: + return "无效的模板编号" + case 85043: + return "模板错误" + case 85044: + return "代码包超过大小限制" + case 85045: + return "ext_json 有不存在的路径" + case 85046: + return "tabBar 中缺少 path" + case 85047: + return "pages 字段为空" + case 85048: + return "ext_json 解析失败" + case 80082: + return "没有权限使用该插件" + case 80067: + return "找不到使用的插件" + case 80066: + return "非法的插件版本" + case 9402202: + return "请勿频繁提交,待上一次操作完成后再提交" + case 9402203: + return `标准模板ext_json错误,传了不合法的参数, 如果是标准模板库的模板,则ext_json支持的参数仅为{"extAppid":'', "ext": {}, "window": {}}` + } + return "系统繁忙" +} diff --git a/service/wechatopen/wxa.deletetemplate.go b/service/wechatopen/wxa.deletetemplate.go new file mode 100644 index 00000000..2dbc3fe8 --- /dev/null +++ b/service/wechatopen/wxa.deletetemplate.go @@ -0,0 +1,46 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaDeleteTemplateResponse struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` +} + +type WxaDeleteTemplateResult struct { + Result WxaDeleteTemplateResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaDeleteTemplateResult(result WxaDeleteTemplateResponse, body []byte, err error) *WxaDeleteTemplateResult { + return &WxaDeleteTemplateResult{Result: result, Body: body, Err: err} +} + +// WxaDeleteTemplate 删除指定代码模板 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/code_template/deletetemplate.html +func (app *App) WxaDeleteTemplate(templateId string) *WxaDeleteTemplateResult { + app.componentAccessToken = app.GetComponentAccessToken() + // 参数 + params := NewParams() + params.Set("template_id", templateId) + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/deletetemplate?access_token=%s", app.componentAccessToken), params, http.MethodPost) + // 定义 + var response WxaDeleteTemplateResponse + err = json.Unmarshal(body, &response) + return NewWxaDeleteTemplateResult(response, body, err) +} + +// ErrcodeInfo 错误描述 +func (resp *WxaDeleteTemplateResult) ErrcodeInfo() string { + switch resp.Result.Errcode { + case 85064: + return "找不到模板,请检查模板id是否输入正确" + } + return "系统繁忙" +} diff --git a/service/wechatopen/wxa.get_auditstatus.go b/service/wechatopen/wxa.get_auditstatus.go new file mode 100644 index 00000000..100bafd1 --- /dev/null +++ b/service/wechatopen/wxa.get_auditstatus.go @@ -0,0 +1,54 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaGetAuditStatusResponse struct { + Errcode int `json:"errcode"` // 返回码 + Errmsg string `json:"errmsg"` // 错误信息 + Auditid int `json:"auditid"` // 最新的审核 ID + Status int `json:"status"` // 审核状态 + Reason string `json:"reason"` // 当审核被拒绝时,返回的拒绝原因 + ScreenShot string `json:"ScreenShot"` // 当审核被拒绝时,会返回审核失败的小程序截图示例。用 | 分隔的 media_id 的列表,可通过获取永久素材接口拉取截图内容 +} + +type WxaGetAuditStatusResult struct { + Result WxaGetAuditStatusResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaGetAuditStatusResult(result WxaGetAuditStatusResponse, body []byte, err error) *WxaGetAuditStatusResult { + return &WxaGetAuditStatusResult{Result: result, Body: body, Err: err} +} + +// WxaGetAuditStatus 查询指定发布审核单的审核状态 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/code/get_auditstatus.html +func (app *App) WxaGetAuditStatus(auditid int64) *WxaGetAuditStatusResult { + app.authorizerAccessToken = app.GetAuthorizerAccessToken() + // 参数 + params := app.NewParamsWith() + params.Set("auditid", auditid) + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/get_auditstatus?access_token=%s", app.authorizerAccessToken), params, http.MethodPost) + // 定义 + var response WxaGetAuditStatusResponse + err = json.Unmarshal(body, &response) + return NewWxaGetAuditStatusResult(response, body, err) +} + +// ErrcodeInfo 错误描述 +func (resp *WxaGetAuditStatusResult) ErrcodeInfo() string { + switch resp.Result.Errcode { + case 86000: + return "不是由第三方代小程序进行调用" + case 86001: + return "不存在第三方的已经提交的代码" + case 85012: + return "无效的审核 id" + } + return "系统繁忙" +} diff --git a/service/wechatopen/wxa.get_latest_auditstatus.go b/service/wechatopen/wxa.get_latest_auditstatus.go new file mode 100644 index 00000000..293edc1c --- /dev/null +++ b/service/wechatopen/wxa.get_latest_auditstatus.go @@ -0,0 +1,51 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaGetLatestAuditStatusResponse struct { + Errcode int `json:"errcode"` // 返回码 + Errmsg string `json:"errmsg"` // 错误信息 + Auditid int `json:"auditid"` // 最新的审核 ID + Status int `json:"status"` // 审核状态 + Reason string `json:"reason"` // 当审核被拒绝时,返回的拒绝原因 + ScreenShot string `json:"ScreenShot"` // 当审核被拒绝时,会返回审核失败的小程序截图示例。用 | 分隔的 media_id 的列表,可通过获取永久素材接口拉取截图内容 +} + +type WxaGetLatestAuditStatusResult struct { + Result WxaGetLatestAuditStatusResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaGetLatestAuditStatusResult(result WxaGetLatestAuditStatusResponse, body []byte, err error) *WxaGetLatestAuditStatusResult { + return &WxaGetLatestAuditStatusResult{Result: result, Body: body, Err: err} +} + +// WxaGetLatestAuditStatus 查询最新一次提交的审核状态 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/code/get_auditstatus.html +func (app *App) WxaGetLatestAuditStatus() *WxaGetLatestAuditStatusResult { + app.authorizerAccessToken = app.GetAuthorizerAccessToken() + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/get_latest_auditstatus?access_token=%s", app.authorizerAccessToken), map[string]interface{}{}, http.MethodPost) + // 定义 + var response WxaGetLatestAuditStatusResponse + err = json.Unmarshal(body, &response) + return NewWxaGetLatestAuditStatusResult(response, body, err) +} + +// ErrcodeInfo 错误描述 +func (resp *WxaGetLatestAuditStatusResult) ErrcodeInfo() string { + switch resp.Result.Errcode { + case 86000: + return "不是由第三方代小程序进行调用" + case 86001: + return "不存在第三方的已经提交的代码" + case 85012: + return "无效的审核 id" + } + return "系统繁忙" +} diff --git a/service/wechatopen/wxa.get_page.go b/service/wechatopen/wxa.get_page.go new file mode 100644 index 00000000..109968b1 --- /dev/null +++ b/service/wechatopen/wxa.get_page.go @@ -0,0 +1,35 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaGetPageResponse struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` + PageList []string `json:"page_list"` // page_list 页面配置列表 +} + +type WxaGetPageResult struct { + Result WxaGetPageResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaGetPageResult(result WxaGetPageResponse, body []byte, err error) *WxaGetPageResult { + return &WxaGetPageResult{Result: result, Body: body, Err: err} +} + +// WxaGetPage 获取已上传的代码的页面列表 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/code/get_page.html +func (app *App) WxaGetPage() *WxaGetPageResult { + app.authorizerAccessToken = app.GetAuthorizerAccessToken() + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/get_page?access_token=%s", app.authorizerAccessToken), map[string]interface{}{}, http.MethodGet) + // 定义 + var response WxaGetPageResponse + err = json.Unmarshal(body, &response) + return NewWxaGetPageResult(response, body, err) +} diff --git a/service/wechatopen/wxa.get_qrcode.go b/service/wechatopen/wxa.get_qrcode.go new file mode 100644 index 00000000..1a3e978e --- /dev/null +++ b/service/wechatopen/wxa.get_qrcode.go @@ -0,0 +1,39 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaGetQrcodeResponse struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` +} + +type WxaGetQrcodeResult struct { + Result WxaGetQrcodeResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaGetQrcodeResult(result WxaGetQrcodeResponse, body []byte, err error) *WxaGetQrcodeResult { + return &WxaGetQrcodeResult{Result: result, Body: body, Err: err} +} + +// WxaGetQrcode 获取体验版二维码 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/code/get_qrcode.html +func (app *App) WxaGetQrcode(path string) *WxaGetQrcodeResult { + app.authorizerAccessToken = app.GetAuthorizerAccessToken() + // 参数 + params := NewParams() + if path != "" { + params["path"] = path // 指定二维码扫码后直接进入指定页面并可同时带上参数) + } + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/get_qrcode?access_token=%s", app.authorizerAccessToken), params, http.MethodGet) + // 定义 + var response WxaGetQrcodeResponse + err = json.Unmarshal(body, &response) + return NewWxaGetQrcodeResult(response, body, err) +} diff --git a/service/wechatopen/wxa.gettemplatedraftlist.go b/service/wechatopen/wxa.gettemplatedraftlist.go new file mode 100644 index 00000000..86aead13 --- /dev/null +++ b/service/wechatopen/wxa.gettemplatedraftlist.go @@ -0,0 +1,44 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaGetTemplateDraftListResponse struct { + Errcode int `json:"errcode"` // 返回码 + Errmsg string `json:"errmsg"` // 错误信息 + DraftList []struct { + CreateTime int `json:"create_time"` // 开发者上传草稿时间戳 + UserVersion string `json:"user_version"` // 版本号,开发者自定义字段 + UserDesc string `json:"user_desc"` // 版本描述 开发者自定义字段 + DraftId int `json:"draft_id"` // 草稿 id + SourceMiniprogramAppid string `json:"source_miniprogram_appid"` + SourceMiniprogram string `json:"source_miniprogram"` + Developer string `json:"developer"` + CategoryList []interface{} `json:"category_list"` + } `json:"draft_list"` // 草稿信息列表 +} + +type WxaGetTemplateDraftListResult struct { + Result WxaGetTemplateDraftListResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaGetTemplateDraftListResult(result WxaGetTemplateDraftListResponse, body []byte, err error) *WxaGetTemplateDraftListResult { + return &WxaGetTemplateDraftListResult{Result: result, Body: body, Err: err} +} + +// WxaGetTemplateDraftList 获取代码草稿列表 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/code_template/gettemplatedraftlist.html +func (app *App) WxaGetTemplateDraftList() *WxaGetTemplateDraftListResult { + app.componentAccessToken = app.GetComponentAccessToken() + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/gettemplatedraftlist?access_token=%s", app.componentAccessToken), map[string]interface{}{}, http.MethodGet) + // 定义 + var response WxaGetTemplateDraftListResponse + err = json.Unmarshal(body, &response) + return NewWxaGetTemplateDraftListResult(response, body, err) +} diff --git a/service/wechatopen/wxa.gettemplatelist.go b/service/wechatopen/wxa.gettemplatelist.go new file mode 100644 index 00000000..75088f0f --- /dev/null +++ b/service/wechatopen/wxa.gettemplatelist.go @@ -0,0 +1,45 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaGetTemplateListResponse struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` + TemplateList []struct { + CreateTime int `json:"create_time"` // 被添加为模板的时间 + UserVersion string `json:"user_version"` // 模板版本号,开发者自定义字段 + UserDesc string `json:"user_desc"` // 模板描述,开发者自定义字段 + TemplateId int64 `json:"template_id"` // 模板 id + TemplateType int `json:"template_type"` // 0对应普通模板,1对应标准模板 + SourceMiniprogramAppid string `json:"source_miniprogram_appid"` // 开发小程序的appid + SourceMiniprogram string `json:"source_miniprogram"` // 开发小程序的名称 + Developer string `json:"developer"` // 开发者 + CategoryList []interface{} `json:"category_list"` + } `json:"template_list"` // 模板信息列表 +} + +type WxaGetTemplateListResult struct { + Result WxaGetTemplateListResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaGetTemplateListResult(result WxaGetTemplateListResponse, body []byte, err error) *WxaGetTemplateListResult { + return &WxaGetTemplateListResult{Result: result, Body: body, Err: err} +} + +// WxaGetTemplateList 获取代码模板列表 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/code_template/gettemplatelist.html +func (app *App) WxaGetTemplateList() *WxaGetTemplateListResult { + app.componentAccessToken = app.GetComponentAccessToken() + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/gettemplatelist?access_token=%s", app.componentAccessToken), map[string]interface{}{}, http.MethodGet) + // 定义 + var response WxaGetTemplateListResponse + err = json.Unmarshal(body, &response) + return NewWxaGetTemplateListResult(response, body, err) +} diff --git a/service/wechatopen/wxa.memberauth.go b/service/wechatopen/wxa.memberauth.go new file mode 100644 index 00000000..0ec29281 --- /dev/null +++ b/service/wechatopen/wxa.memberauth.go @@ -0,0 +1,40 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaMemberAuthResponse struct { + Errcode int `json:"errcode"` // 错误码 + Errmsg string `json:"errmsg"` // 错误信息 + Members []struct { + Userstr string `json:"userstr"` // 人员对应的唯一字符串 + } `json:"members"` // 人员信息列表 +} + +type WxaMemberAuthResult struct { + Result WxaMemberAuthResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaMemberAuthResult(result WxaMemberAuthResponse, body []byte, err error) *WxaMemberAuthResult { + return &WxaMemberAuthResult{Result: result, Body: body, Err: err} +} + +// WxaMemberAuth 获取体验者列表 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Mini_Program_AdminManagement/memberauth.html +func (app *App) WxaMemberAuth() *WxaMemberAuthResult { + app.authorizerAccessToken = app.GetAuthorizerAccessToken() + // 参数 + params := NewParams() + params["action"] = "get_experiencer" + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/memberauth?access_token=%s", app.authorizerAccessToken), params, http.MethodPost) + // 定义 + var response WxaMemberAuthResponse + err = json.Unmarshal(body, &response) + return NewWxaMemberAuthResult(response, body, err) +} diff --git a/service/wechatopen/wxa.modify_domain.go b/service/wechatopen/wxa.modify_domain.go new file mode 100644 index 00000000..49423238 --- /dev/null +++ b/service/wechatopen/wxa.modify_domain.go @@ -0,0 +1,49 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaModifyDomainResponse struct { + Errcode int `json:"errcode"` // 错误码 + Errmsg string `json:"errmsg"` // 错误信息 + Requestdomain []string `json:"requestdomain"` // request 合法域名 + Wsrequestdomain []string `json:"wsrequestdomain"` // socket 合法域名 + Uploaddomain []string `json:"uploaddomain"` // uploadFile 合法域名 + Downloaddomain []string `json:"downloaddomain"` // downloadFile 合法域名 + Udpdomain []string `json:"udpdomain"` // udp 合法域名 + Tcpdomain []string `json:"tcpdomain"` // tcp 合法域名 + InvalidRequestdomain []string `json:"invalid_requestdomain"` // request 不合法域名 + InvalidWsrequestdomain []string `json:"invalid_wsrequestdomain"` // socket 不合法域名 + InvalidUploaddomain []string `json:"invalid_uploaddomain"` // uploadFile 不合法域名 + InvalidDownloaddomain []string `json:"invalid_downloaddomain"` // downloadFile 不合法域名 + InvalidUdpdomain []string `json:"invalid_udpdomain"` // udp 不合法域名 + InvalidTcpdomain []string `json:"invalid_tcpdomain"` // tcp 不合法域名 + NoIcpDomain []string `json:"no_icp_domain"` // 没有经过icp备案的域名 +} + +type WxaModifyDomainResult struct { + Result WxaModifyDomainResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaModifyDomainResult(result WxaModifyDomainResponse, body []byte, err error) *WxaModifyDomainResult { + return &WxaModifyDomainResult{Result: result, Body: body, Err: err} +} + +// WxaModifyDomain 设置服务器域名 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Mini_Program_Basic_Info/Server_Address_Configuration.html +func (app *App) WxaModifyDomain(notMustParams ...Params) *WxaModifyDomainResult { + app.authorizerAccessToken = app.GetAuthorizerAccessToken() + // 参数 + params := app.NewParamsWith(notMustParams...) + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/modify_domain?access_token=%s", app.authorizerAccessToken), params, http.MethodPost) + // 定义 + var response WxaModifyDomainResponse + err = json.Unmarshal(body, &response) + return NewWxaModifyDomainResult(response, body, err) +} diff --git a/service/wechatopen/wxa.release.go b/service/wechatopen/wxa.release.go new file mode 100644 index 00000000..bc17b569 --- /dev/null +++ b/service/wechatopen/wxa.release.go @@ -0,0 +1 @@ +package wechatopen diff --git a/service/wechatopen/wxa.revertcoderelease.go b/service/wechatopen/wxa.revertcoderelease.go new file mode 100644 index 00000000..bc17b569 --- /dev/null +++ b/service/wechatopen/wxa.revertcoderelease.go @@ -0,0 +1 @@ +package wechatopen diff --git a/service/wechatopen/wxa.submit_audit.go b/service/wechatopen/wxa.submit_audit.go new file mode 100644 index 00000000..b75ccf18 --- /dev/null +++ b/service/wechatopen/wxa.submit_audit.go @@ -0,0 +1,36 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaSubmitAuditResponse struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` +} + +type WxaSubmitAuditResult struct { + Result WxaSubmitAuditResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaSubmitAuditResult(result WxaSubmitAuditResponse, body []byte, err error) *WxaSubmitAuditResult { + return &WxaSubmitAuditResult{Result: result, Body: body, Err: err} +} + +// WxaSubmitAudit 提交审核 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/code/submit_audit.html +func (app *App) WxaSubmitAudit(notMustParams ...Params) *WxaSubmitAuditResult { + app.authorizerAccessToken = app.GetAuthorizerAccessToken() + // 参数 + params := app.NewParamsWith(notMustParams...) + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/submit_audit?access_token=%s", app.authorizerAccessToken), params, http.MethodPost) + // 定义 + var response WxaSubmitAuditResponse + err = json.Unmarshal(body, &response) + return NewWxaSubmitAuditResult(response, body, err) +} diff --git a/service/wechatopen/wxa.unbind_tester.go b/service/wechatopen/wxa.unbind_tester.go new file mode 100644 index 00000000..f1e2787f --- /dev/null +++ b/service/wechatopen/wxa.unbind_tester.go @@ -0,0 +1,40 @@ +package wechatopen + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type WxaUnbindTesterResponse struct { + Errcode int `json:"errcode"` // 错误码 + Errmsg string `json:"errmsg"` // 错误信息 +} + +type WxaUnbindTesterResult struct { + Result WxaUnbindTesterResponse // 结果 + Body []byte // 内容 + Err error // 错误 +} + +func NewWxaUnbindTesterResult(result WxaUnbindTesterResponse, body []byte, err error) *WxaUnbindTesterResult { + return &WxaUnbindTesterResult{Result: result, Body: body, Err: err} +} + +// WxaUnbindTester 解除绑定体验者 +// https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Mini_Program_AdminManagement/unbind_tester.html +func (app *App) WxaUnbindTester(wechatid, userstr string) *WxaUnbindTesterResult { + app.authorizerAccessToken = app.GetAuthorizerAccessToken() + // 参数 + params := NewParams() + if wechatid != "" { + params["wechatid"] = wechatid + } + params["userstr"] = userstr + // 请求 + body, err := app.request(fmt.Sprintf("https://api.weixin.qq.com/wxa/unbind_tester?access_token=%s", app.authorizerAccessToken), params, http.MethodPost) + // 定义 + var response WxaUnbindTesterResponse + err = json.Unmarshal(body, &response) + return NewWxaUnbindTesterResult(response, body, err) +} diff --git a/service/wechatopen/wxa.undocodeaudit.go b/service/wechatopen/wxa.undocodeaudit.go new file mode 100644 index 00000000..bc17b569 --- /dev/null +++ b/service/wechatopen/wxa.undocodeaudit.go @@ -0,0 +1 @@ +package wechatopen diff --git a/service/wechatopen/xml.go b/service/wechatopen/xml.go new file mode 100644 index 00000000..8181bbb8 --- /dev/null +++ b/service/wechatopen/xml.go @@ -0,0 +1,43 @@ +package wechatopen + +import ( + "encoding/xml" + "io" + "strings" +) + +func XmlDecode(data string) map[string]string { + decoder := xml.NewDecoder(strings.NewReader(data)) + result := make(map[string]string) + key := "" + for { + token, err := decoder.Token() //读取一个标签或者文本内容 + if err == io.EOF { + return result + } + if err != nil { + return result + } + switch tp := token.(type) { //读取的TOKEN可以是以下三种类型:StartElement起始标签,EndElement结束标签,CharData文本内容 + case xml.StartElement: + se := xml.StartElement(tp) //强制类型转换 + if se.Name.Local != "xml" { + key = se.Name.Local + } + if len(se.Attr) != 0 { + //读取标签属性 + } + case xml.EndElement: + ee := xml.EndElement(tp) + if ee.Name.Local == "xml" { + return result + } + case xml.CharData: //文本数据,注意一个结束标签和另一个起始标签之间可能有空格 + cd := xml.CharData(tp) + data := strings.TrimSpace(string(cd)) + if len(data) != 0 { + result[key] = data + } + } + } +}