123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507 |
- // Copyright 2021 Tencent Inc. All rights reserved.
- // Package core 微信支付 API v3 Go SDK HTTPClient 基础库,你可以使用它来创建一个 Client,并向微信支付发送 HTTP 请求
- //
- // 初始化 Client 时,你需要指定以下参数:
- // - Credential 用于生成 HTTP Header 中的 Authorization 信息,微信支付 API v3依赖该值来保证请求的真实性和数据的完整性
- // - Validator 用于对微信支付的应答进行校验,避免被恶意攻击
- package core
- import (
- "bytes"
- "context"
- "encoding/json"
- "encoding/xml"
- "fmt"
- "io"
- "io/ioutil"
- "mime/multipart"
- "net/http"
- "net/textproto"
- "net/url"
- "os"
- "reflect"
- "regexp"
- "runtime"
- "strings"
- "time"
- "github.com/wechatpay-apiv3/wechatpay-go/core/auth"
- "github.com/wechatpay-apiv3/wechatpay-go/core/auth/credentials"
- "github.com/wechatpay-apiv3/wechatpay-go/core/cipher"
- "github.com/wechatpay-apiv3/wechatpay-go/core/consts"
- )
- var (
- regJSONTypeCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:vnd\.[^;]+\+)?json)`)
- regXMLTypeCheck = regexp.MustCompile(`(?i:(?:application|text)/xml)`)
- )
- // APIResult 微信支付API v3 请求结果
- type APIResult struct {
- // 本次请求所使用的 HTTPRequest
- Request *http.Request
- // 本次请求所获得的 HTTPResponse
- Response *http.Response
- }
- // ClientOption 微信支付 API v3 HTTPClient core.Client 初始化参数
- type ClientOption interface {
- // Apply 将初始化参数应用到 DialSettings 中
- Apply(settings *DialSettings) error
- }
- // ErrorOption 错误初始化参数,用于返回错误
- type ErrorOption struct{ Error error }
- // Apply 返回初始化错误
- func (w ErrorOption) Apply(*DialSettings) error {
- return w.Error
- }
- // Client 微信支付API v3 基础 Client
- type Client struct {
- httpClient *http.Client
- credential auth.Credential
- validator auth.Validator
- signer auth.Signer
- cipher cipher.Cipher
- }
- // NewClient 初始化一个微信支付API v3 HTTPClient
- //
- // 初始化的时候你可以传递多个配置信息
- func NewClient(ctx context.Context, opts ...ClientOption) (*Client, error) {
- settings, err := initSettings(opts)
- if err != nil {
- return nil, fmt.Errorf("init client setting err:%v", err)
- }
- client := initClientWithSettings(ctx, settings)
- return client, nil
- }
- // NewClientWithDialSettings 使用 DialSettings 初始化一个微信支付API v3 HTTPClient
- func NewClientWithDialSettings(ctx context.Context, settings *DialSettings) (*Client, error) {
- if err := settings.Validate(); err != nil {
- return nil, err
- }
- client := initClientWithSettings(ctx, settings)
- return client, nil
- }
- // NewClientWithValidator 使用原 Client 复制一个新的 Client,并设置新 Client 的 validator。
- // 原 Client 不受任何影响
- func NewClientWithValidator(client *Client, validator auth.Validator) *Client {
- return &Client{
- httpClient: client.httpClient,
- credential: client.credential,
- signer: client.signer,
- validator: validator,
- cipher: client.cipher,
- }
- }
- func initClientWithSettings(_ context.Context, settings *DialSettings) *Client {
- client := &Client{
- signer: settings.Signer,
- validator: settings.Validator,
- credential: &credentials.WechatPayCredentials{Signer: settings.Signer},
- httpClient: settings.HTTPClient,
- cipher: settings.Cipher,
- }
- if client.httpClient == nil {
- client.httpClient = &http.Client{
- Timeout: consts.DefaultTimeout,
- }
- }
- return client
- }
- func initSettings(opts []ClientOption) (*DialSettings, error) {
- var (
- o DialSettings
- err error
- )
- for _, opt := range opts {
- if err = opt.Apply(&o); err != nil {
- return nil, err
- }
- }
- if err := o.Validate(); err != nil {
- return nil, err
- }
- return &o, nil
- }
- // Get 向微信支付发送一个 HTTP Get 请求
- func (client *Client) Get(ctx context.Context, requestURL string) (*APIResult, error) {
- return client.doRequest(ctx, http.MethodGet, requestURL, nil, consts.ApplicationJSON, nil, "")
- }
- // Post 向微信支付发送一个 HTTP Post 请求
- func (client *Client) Post(ctx context.Context, requestURL string, requestBody interface{}) (*APIResult, error) {
- return client.requestWithJSONBody(ctx, http.MethodPost, requestURL, requestBody)
- }
- // Patch 向微信支付发送一个 HTTP Patch 请求
- func (client *Client) Patch(ctx context.Context, requestURL string, requestBody interface{}) (*APIResult, error) {
- return client.requestWithJSONBody(ctx, http.MethodPatch, requestURL, requestBody)
- }
- // Put 向微信支付发送一个 HTTP Put 请求
- func (client *Client) Put(ctx context.Context, requestURL string, requestBody interface{}) (*APIResult, error) {
- return client.requestWithJSONBody(ctx, http.MethodPut, requestURL, requestBody)
- }
- // Delete 向微信支付发送一个 HTTP Delete 请求
- func (client *Client) Delete(ctx context.Context, requestURL string, requestBody interface{}) (*APIResult, error) {
- return client.requestWithJSONBody(ctx, http.MethodDelete, requestURL, requestBody)
- }
- // Upload 向微信支付发送上传文件
- // 推荐使用 services/fileuploader 中各上传接口的实现
- func (client *Client) Upload(ctx context.Context, requestURL, meta, reqBody, formContentType string) (
- *APIResult, error,
- ) {
- return client.doRequest(ctx, http.MethodPost, requestURL, nil, formContentType, strings.NewReader(reqBody), meta)
- }
- func (client *Client) requestWithJSONBody(ctx context.Context, method, requestURL string, body interface{}) (
- *APIResult, error,
- ) {
- reqBody, err := setBody(body, consts.ApplicationJSON)
- if err != nil {
- return nil, err
- }
- return client.doRequest(ctx, method, requestURL, nil, consts.ApplicationJSON, reqBody, reqBody.String())
- }
- func (client *Client) doRequest(
- ctx context.Context,
- method string,
- requestURL string,
- header http.Header,
- contentType string,
- reqBody io.Reader,
- signBody string,
- ) (*APIResult, error) {
- var (
- err error
- authorization string
- request *http.Request
- )
- // Construct Request
- if request, err = http.NewRequestWithContext(ctx, method, requestURL, reqBody); err != nil {
- return nil, err
- }
- // Header Setting Priority:
- // Fixed Headers > Per-Request Header Parameters
- // Add Request Header Parameters
- for key, values := range header {
- for _, v := range values {
- request.Header.Add(key, v)
- }
- }
- // Set Fixed Headers
- request.Header.Set(consts.Accept, "*/*")
- request.Header.Set(consts.ContentType, contentType)
- ua := fmt.Sprintf(consts.UserAgentFormat, consts.Version, runtime.GOOS, runtime.Version())
- request.Header.Set(consts.UserAgent, ua)
- // Set Authentication
- if authorization, err = client.credential.GenerateAuthorizationHeader(
- ctx, method, request.URL.RequestURI(),
- signBody,
- ); err != nil {
- return nil, fmt.Errorf("generate authorization err:%s", err.Error())
- }
- request.Header.Set(consts.Authorization, authorization)
- // indicate Wechatpay-Serial that client can verify
- if serial, err := client.validator.GetAcceptSerial(ctx); err == nil {
- request.Header.Set(consts.WechatPaySerial, serial)
- }
- // Send HTTP Request
- result, err := client.doHTTP(request)
- if err != nil {
- return result, err
- }
- // Check if Success
- if err = CheckResponse(result.Response); err != nil {
- return result, err
- }
- // Validate WechatPay Signature
- if err = client.validator.Validate(ctx, result.Response); err != nil {
- return result, err
- }
- return result, nil
- }
- // Request 向微信支付发送请求
- //
- // 相比于 Get / Post / Put / Patch / Delete 方法,本方法可以设置更多内容
- // 特别地,如果需要为当前请求设置 Header,可以使用本方法
- func (client *Client) Request(
- ctx context.Context,
- method, requestPath string,
- headerParams http.Header,
- queryParams url.Values,
- postBody interface{},
- contentType string,
- ) (result *APIResult, err error) {
- // Setup path and query parameters
- varURL, err := url.Parse(requestPath)
- if err != nil {
- return nil, err
- }
- // Adding Query Param
- query := varURL.Query()
- for k, values := range queryParams {
- for _, v := range values {
- query.Add(k, v)
- }
- }
- // Encode the parameters.
- varURL.RawQuery = query.Encode()
- if postBody == nil {
- return client.doRequest(ctx, method, varURL.String(), headerParams, contentType, nil, "")
- }
- // Detect postBody type and set body content
- if contentType == "" {
- contentType = consts.ApplicationJSON
- }
- var body *bytes.Buffer
- body, err = setBody(postBody, contentType)
- if err != nil {
- return nil, err
- }
- return client.doRequest(ctx, method, varURL.String(), headerParams, contentType, body, body.String())
- }
- func (client *Client) doHTTP(req *http.Request) (result *APIResult, err error) {
- result = &APIResult{
- Request: req,
- }
- result.Response, err = client.httpClient.Do(req)
- return result, err
- }
- // EncryptRequest 使用 cipher 对请求结构进行原地加密,并返回加密所用的平台证书的序列号。
- // 未设置 cipher 时将跳过加密,并返回空序列号。
- //
- // 本方法会对结构中的敏感字段进行原地加密,因此需要传入结构体的指针。
- func (client *Client) EncryptRequest(ctx context.Context, req interface{}) (string, error) {
- if client.cipher == nil {
- return "", nil
- }
- return client.cipher.Encrypt(ctx, req)
- }
- // DecryptResponse 使用 cipher 对应答结构进行原地解密,未设置 cipher 时将跳过解密
- //
- // 本方法会对结构中的敏感字段进行原地解密,因此需要传入结构体的指针。
- func (client *Client) DecryptResponse(ctx context.Context, resp interface{}) error {
- if client.cipher == nil {
- return nil
- }
- return client.cipher.Decrypt(ctx, resp)
- }
- // Sign 使用 signer 对字符串进行签名
- func (client *Client) Sign(ctx context.Context, message string) (result *auth.SignatureResult, err error) {
- return client.signer.Sign(ctx, message)
- }
- // CheckResponse 校验请求是否成功
- //
- // 当http回包的状态码的范围不是200-299之间的时候,会返回相应的错误信息,主要包括http状态码、回包错误码、回包错误信息提示
- func CheckResponse(resp *http.Response) error {
- if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
- return nil
- }
- slurp, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return fmt.Errorf("invalid response, read body error: %w", err)
- }
- _ = resp.Body.Close()
- resp.Body = ioutil.NopCloser(bytes.NewBuffer(slurp))
- apiError := &APIError{
- StatusCode: resp.StatusCode,
- Header: resp.Header,
- Body: string(slurp),
- }
- // 忽略 JSON 解析错误,均返回 apiError
- _ = json.Unmarshal(slurp, apiError)
- return apiError
- }
- // UnMarshalResponse 将回包组织成结构化数据
- func UnMarshalResponse(httpResp *http.Response, resp interface{}) error {
- body, err := ioutil.ReadAll(httpResp.Body)
- _ = httpResp.Body.Close()
- if err != nil {
- return err
- }
- httpResp.Body = ioutil.NopCloser(bytes.NewBuffer(body))
- err = json.Unmarshal(body, resp)
- if err != nil {
- return err
- }
- return nil
- }
- // CreateFormField 设置form-data 中的普通属性
- //
- // 示例内容
- //
- // Content-Disposition: form-data; name="meta";
- // Content-Type: application/json
- //
- // { "filename": "file_test.mp4", "sha256": " hjkahkjsjkfsjk78687dhjahdajhk " }
- //
- // 如果要设置上述内容
- //
- // CreateFormField(w, "meta", "application/json", meta)
- func CreateFormField(w *multipart.Writer, fieldName, contentType string, fieldValue []byte) error {
- h := make(textproto.MIMEHeader)
- h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s";`, fieldName))
- h.Set("Content-Type", contentType)
- part, err := w.CreatePart(h)
- if err != nil {
- return err
- }
- _, err = part.Write(fieldValue)
- return err
- }
- // CreateFormFile 设置form-data中的文件
- //
- // 示例内容:
- //
- // Content-Disposition: form-data; name="file"; filename="file_test.mp4";
- // Content-Type: video/mp4
- //
- // pic1 //pic1即为媒体视频的二进制内容
- //
- // 如果要设置上述内容,则CreateFormFile(w, "file_test.mp4", "video/mp4", pic1)
- func CreateFormFile(w *multipart.Writer, filename, contentType string, file []byte) error {
- h := make(textproto.MIMEHeader)
- h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "file", filename))
- h.Set("Content-Type", contentType)
- part, err := w.CreatePart(h)
- if err != nil {
- return err
- }
- _, err = part.Write(file)
- return err
- }
- // setBody Set Request body from an interface
- //
- //revive:disable-next-line:cyclomatic 本函数实现需要考虑多种情况,但理解起来并不复杂,进行圈复杂度豁免
- func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) {
- bodyBuf = &bytes.Buffer{}
- switch b := body.(type) {
- case string:
- _, err = bodyBuf.WriteString(b)
- case *string:
- _, err = bodyBuf.WriteString(*b)
- case []byte:
- _, err = bodyBuf.Write(b)
- case **os.File:
- _, err = bodyBuf.ReadFrom(*b)
- case io.Reader:
- _, err = bodyBuf.ReadFrom(b)
- default:
- if regJSONTypeCheck.MatchString(contentType) {
- err = json.NewEncoder(bodyBuf).Encode(body)
- } else if regXMLTypeCheck.MatchString(contentType) {
- err = xml.NewEncoder(bodyBuf).Encode(body)
- }
- }
- if err != nil {
- return nil, err
- }
- if bodyBuf.Len() == 0 {
- err = fmt.Errorf("invalid body type %s", contentType)
- return nil, err
- }
- return bodyBuf, nil
- }
- // contains is a case-insensitive match, finding needle in a haystack
- func contains(haystack []string, needle string) bool {
- for _, a := range haystack {
- if strings.EqualFold(a, needle) {
- return true
- }
- }
- return false
- }
- // SelectHeaderContentType select a content type from the available list.
- func SelectHeaderContentType(contentTypes []string) string {
- if len(contentTypes) == 0 {
- return consts.ApplicationJSON
- }
- if contains(contentTypes, consts.ApplicationJSON) {
- return consts.ApplicationJSON
- }
- return contentTypes[0] // use the first content type specified in 'consumes'
- }
- // ParameterToString 将参数转换为字符串,并使用指定分隔符分隔列表参数
- func ParameterToString(obj interface{}, collectionFormat string) string {
- var delimiter string
- switch collectionFormat {
- case "pipes":
- delimiter = "|"
- case "ssv":
- delimiter = " "
- case "tsv":
- delimiter = "\t"
- case "csv":
- delimiter = ","
- }
- if reflect.TypeOf(obj).Kind() == reflect.Slice {
- return strings.Trim(strings.Replace(fmt.Sprint(obj), " ", delimiter, -1), "[]")
- } else if t, ok := obj.(time.Time); ok {
- return t.Format(time.RFC3339)
- }
- return fmt.Sprintf("%v", obj)
- }
- // ParameterToJSON 将参数转换为 Json 字符串
- func ParameterToJSON(obj interface{}) (string, error) {
- jsonBuf, err := json.Marshal(obj)
- if err != nil {
- return "", err
- }
- return string(jsonBuf), err
- }
|