client.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. // Copyright 2021 Tencent Inc. All rights reserved.
  2. // Package core 微信支付 API v3 Go SDK HTTPClient 基础库,你可以使用它来创建一个 Client,并向微信支付发送 HTTP 请求
  3. //
  4. // 初始化 Client 时,你需要指定以下参数:
  5. // - Credential 用于生成 HTTP Header 中的 Authorization 信息,微信支付 API v3依赖该值来保证请求的真实性和数据的完整性
  6. // - Validator 用于对微信支付的应答进行校验,避免被恶意攻击
  7. package core
  8. import (
  9. "bytes"
  10. "context"
  11. "encoding/json"
  12. "encoding/xml"
  13. "fmt"
  14. "io"
  15. "io/ioutil"
  16. "mime/multipart"
  17. "net/http"
  18. "net/textproto"
  19. "net/url"
  20. "os"
  21. "reflect"
  22. "regexp"
  23. "runtime"
  24. "strings"
  25. "time"
  26. "github.com/wechatpay-apiv3/wechatpay-go/core/auth"
  27. "github.com/wechatpay-apiv3/wechatpay-go/core/auth/credentials"
  28. "github.com/wechatpay-apiv3/wechatpay-go/core/cipher"
  29. "github.com/wechatpay-apiv3/wechatpay-go/core/consts"
  30. )
  31. var (
  32. regJSONTypeCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:vnd\.[^;]+\+)?json)`)
  33. regXMLTypeCheck = regexp.MustCompile(`(?i:(?:application|text)/xml)`)
  34. )
  35. // APIResult 微信支付API v3 请求结果
  36. type APIResult struct {
  37. // 本次请求所使用的 HTTPRequest
  38. Request *http.Request
  39. // 本次请求所获得的 HTTPResponse
  40. Response *http.Response
  41. }
  42. // ClientOption 微信支付 API v3 HTTPClient core.Client 初始化参数
  43. type ClientOption interface {
  44. // Apply 将初始化参数应用到 DialSettings 中
  45. Apply(settings *DialSettings) error
  46. }
  47. // ErrorOption 错误初始化参数,用于返回错误
  48. type ErrorOption struct{ Error error }
  49. // Apply 返回初始化错误
  50. func (w ErrorOption) Apply(*DialSettings) error {
  51. return w.Error
  52. }
  53. // Client 微信支付API v3 基础 Client
  54. type Client struct {
  55. httpClient *http.Client
  56. credential auth.Credential
  57. validator auth.Validator
  58. signer auth.Signer
  59. cipher cipher.Cipher
  60. }
  61. // NewClient 初始化一个微信支付API v3 HTTPClient
  62. //
  63. // 初始化的时候你可以传递多个配置信息
  64. func NewClient(ctx context.Context, opts ...ClientOption) (*Client, error) {
  65. settings, err := initSettings(opts)
  66. if err != nil {
  67. return nil, fmt.Errorf("init client setting err:%v", err)
  68. }
  69. client := initClientWithSettings(ctx, settings)
  70. return client, nil
  71. }
  72. // NewClientWithDialSettings 使用 DialSettings 初始化一个微信支付API v3 HTTPClient
  73. func NewClientWithDialSettings(ctx context.Context, settings *DialSettings) (*Client, error) {
  74. if err := settings.Validate(); err != nil {
  75. return nil, err
  76. }
  77. client := initClientWithSettings(ctx, settings)
  78. return client, nil
  79. }
  80. // NewClientWithValidator 使用原 Client 复制一个新的 Client,并设置新 Client 的 validator。
  81. // 原 Client 不受任何影响
  82. func NewClientWithValidator(client *Client, validator auth.Validator) *Client {
  83. return &Client{
  84. httpClient: client.httpClient,
  85. credential: client.credential,
  86. signer: client.signer,
  87. validator: validator,
  88. cipher: client.cipher,
  89. }
  90. }
  91. func initClientWithSettings(_ context.Context, settings *DialSettings) *Client {
  92. client := &Client{
  93. signer: settings.Signer,
  94. validator: settings.Validator,
  95. credential: &credentials.WechatPayCredentials{Signer: settings.Signer},
  96. httpClient: settings.HTTPClient,
  97. cipher: settings.Cipher,
  98. }
  99. if client.httpClient == nil {
  100. client.httpClient = &http.Client{
  101. Timeout: consts.DefaultTimeout,
  102. }
  103. }
  104. return client
  105. }
  106. func initSettings(opts []ClientOption) (*DialSettings, error) {
  107. var (
  108. o DialSettings
  109. err error
  110. )
  111. for _, opt := range opts {
  112. if err = opt.Apply(&o); err != nil {
  113. return nil, err
  114. }
  115. }
  116. if err := o.Validate(); err != nil {
  117. return nil, err
  118. }
  119. return &o, nil
  120. }
  121. // Get 向微信支付发送一个 HTTP Get 请求
  122. func (client *Client) Get(ctx context.Context, requestURL string) (*APIResult, error) {
  123. return client.doRequest(ctx, http.MethodGet, requestURL, nil, consts.ApplicationJSON, nil, "")
  124. }
  125. // Post 向微信支付发送一个 HTTP Post 请求
  126. func (client *Client) Post(ctx context.Context, requestURL string, requestBody interface{}) (*APIResult, error) {
  127. return client.requestWithJSONBody(ctx, http.MethodPost, requestURL, requestBody)
  128. }
  129. // Patch 向微信支付发送一个 HTTP Patch 请求
  130. func (client *Client) Patch(ctx context.Context, requestURL string, requestBody interface{}) (*APIResult, error) {
  131. return client.requestWithJSONBody(ctx, http.MethodPatch, requestURL, requestBody)
  132. }
  133. // Put 向微信支付发送一个 HTTP Put 请求
  134. func (client *Client) Put(ctx context.Context, requestURL string, requestBody interface{}) (*APIResult, error) {
  135. return client.requestWithJSONBody(ctx, http.MethodPut, requestURL, requestBody)
  136. }
  137. // Delete 向微信支付发送一个 HTTP Delete 请求
  138. func (client *Client) Delete(ctx context.Context, requestURL string, requestBody interface{}) (*APIResult, error) {
  139. return client.requestWithJSONBody(ctx, http.MethodDelete, requestURL, requestBody)
  140. }
  141. // Upload 向微信支付发送上传文件
  142. // 推荐使用 services/fileuploader 中各上传接口的实现
  143. func (client *Client) Upload(ctx context.Context, requestURL, meta, reqBody, formContentType string) (
  144. *APIResult, error,
  145. ) {
  146. return client.doRequest(ctx, http.MethodPost, requestURL, nil, formContentType, strings.NewReader(reqBody), meta)
  147. }
  148. func (client *Client) requestWithJSONBody(ctx context.Context, method, requestURL string, body interface{}) (
  149. *APIResult, error,
  150. ) {
  151. reqBody, err := setBody(body, consts.ApplicationJSON)
  152. if err != nil {
  153. return nil, err
  154. }
  155. return client.doRequest(ctx, method, requestURL, nil, consts.ApplicationJSON, reqBody, reqBody.String())
  156. }
  157. func (client *Client) doRequest(
  158. ctx context.Context,
  159. method string,
  160. requestURL string,
  161. header http.Header,
  162. contentType string,
  163. reqBody io.Reader,
  164. signBody string,
  165. ) (*APIResult, error) {
  166. var (
  167. err error
  168. authorization string
  169. request *http.Request
  170. )
  171. // Construct Request
  172. if request, err = http.NewRequestWithContext(ctx, method, requestURL, reqBody); err != nil {
  173. return nil, err
  174. }
  175. // Header Setting Priority:
  176. // Fixed Headers > Per-Request Header Parameters
  177. // Add Request Header Parameters
  178. for key, values := range header {
  179. for _, v := range values {
  180. request.Header.Add(key, v)
  181. }
  182. }
  183. // Set Fixed Headers
  184. request.Header.Set(consts.Accept, "*/*")
  185. request.Header.Set(consts.ContentType, contentType)
  186. ua := fmt.Sprintf(consts.UserAgentFormat, consts.Version, runtime.GOOS, runtime.Version())
  187. request.Header.Set(consts.UserAgent, ua)
  188. // Set Authentication
  189. if authorization, err = client.credential.GenerateAuthorizationHeader(
  190. ctx, method, request.URL.RequestURI(),
  191. signBody,
  192. ); err != nil {
  193. return nil, fmt.Errorf("generate authorization err:%s", err.Error())
  194. }
  195. request.Header.Set(consts.Authorization, authorization)
  196. // indicate Wechatpay-Serial that client can verify
  197. if serial, err := client.validator.GetAcceptSerial(ctx); err == nil {
  198. request.Header.Set(consts.WechatPaySerial, serial)
  199. }
  200. // Send HTTP Request
  201. result, err := client.doHTTP(request)
  202. if err != nil {
  203. return result, err
  204. }
  205. // Check if Success
  206. if err = CheckResponse(result.Response); err != nil {
  207. return result, err
  208. }
  209. // Validate WechatPay Signature
  210. if err = client.validator.Validate(ctx, result.Response); err != nil {
  211. return result, err
  212. }
  213. return result, nil
  214. }
  215. // Request 向微信支付发送请求
  216. //
  217. // 相比于 Get / Post / Put / Patch / Delete 方法,本方法可以设置更多内容
  218. // 特别地,如果需要为当前请求设置 Header,可以使用本方法
  219. func (client *Client) Request(
  220. ctx context.Context,
  221. method, requestPath string,
  222. headerParams http.Header,
  223. queryParams url.Values,
  224. postBody interface{},
  225. contentType string,
  226. ) (result *APIResult, err error) {
  227. // Setup path and query parameters
  228. varURL, err := url.Parse(requestPath)
  229. if err != nil {
  230. return nil, err
  231. }
  232. // Adding Query Param
  233. query := varURL.Query()
  234. for k, values := range queryParams {
  235. for _, v := range values {
  236. query.Add(k, v)
  237. }
  238. }
  239. // Encode the parameters.
  240. varURL.RawQuery = query.Encode()
  241. if postBody == nil {
  242. return client.doRequest(ctx, method, varURL.String(), headerParams, contentType, nil, "")
  243. }
  244. // Detect postBody type and set body content
  245. if contentType == "" {
  246. contentType = consts.ApplicationJSON
  247. }
  248. var body *bytes.Buffer
  249. body, err = setBody(postBody, contentType)
  250. if err != nil {
  251. return nil, err
  252. }
  253. return client.doRequest(ctx, method, varURL.String(), headerParams, contentType, body, body.String())
  254. }
  255. func (client *Client) doHTTP(req *http.Request) (result *APIResult, err error) {
  256. result = &APIResult{
  257. Request: req,
  258. }
  259. result.Response, err = client.httpClient.Do(req)
  260. return result, err
  261. }
  262. // EncryptRequest 使用 cipher 对请求结构进行原地加密,并返回加密所用的平台证书的序列号。
  263. // 未设置 cipher 时将跳过加密,并返回空序列号。
  264. //
  265. // 本方法会对结构中的敏感字段进行原地加密,因此需要传入结构体的指针。
  266. func (client *Client) EncryptRequest(ctx context.Context, req interface{}) (string, error) {
  267. if client.cipher == nil {
  268. return "", nil
  269. }
  270. return client.cipher.Encrypt(ctx, req)
  271. }
  272. // DecryptResponse 使用 cipher 对应答结构进行原地解密,未设置 cipher 时将跳过解密
  273. //
  274. // 本方法会对结构中的敏感字段进行原地解密,因此需要传入结构体的指针。
  275. func (client *Client) DecryptResponse(ctx context.Context, resp interface{}) error {
  276. if client.cipher == nil {
  277. return nil
  278. }
  279. return client.cipher.Decrypt(ctx, resp)
  280. }
  281. // Sign 使用 signer 对字符串进行签名
  282. func (client *Client) Sign(ctx context.Context, message string) (result *auth.SignatureResult, err error) {
  283. return client.signer.Sign(ctx, message)
  284. }
  285. // CheckResponse 校验请求是否成功
  286. //
  287. // 当http回包的状态码的范围不是200-299之间的时候,会返回相应的错误信息,主要包括http状态码、回包错误码、回包错误信息提示
  288. func CheckResponse(resp *http.Response) error {
  289. if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
  290. return nil
  291. }
  292. slurp, err := ioutil.ReadAll(resp.Body)
  293. if err != nil {
  294. return fmt.Errorf("invalid response, read body error: %w", err)
  295. }
  296. _ = resp.Body.Close()
  297. resp.Body = ioutil.NopCloser(bytes.NewBuffer(slurp))
  298. apiError := &APIError{
  299. StatusCode: resp.StatusCode,
  300. Header: resp.Header,
  301. Body: string(slurp),
  302. }
  303. // 忽略 JSON 解析错误,均返回 apiError
  304. _ = json.Unmarshal(slurp, apiError)
  305. return apiError
  306. }
  307. // UnMarshalResponse 将回包组织成结构化数据
  308. func UnMarshalResponse(httpResp *http.Response, resp interface{}) error {
  309. body, err := ioutil.ReadAll(httpResp.Body)
  310. _ = httpResp.Body.Close()
  311. if err != nil {
  312. return err
  313. }
  314. httpResp.Body = ioutil.NopCloser(bytes.NewBuffer(body))
  315. err = json.Unmarshal(body, resp)
  316. if err != nil {
  317. return err
  318. }
  319. return nil
  320. }
  321. // CreateFormField 设置form-data 中的普通属性
  322. //
  323. // 示例内容
  324. //
  325. // Content-Disposition: form-data; name="meta";
  326. // Content-Type: application/json
  327. //
  328. // { "filename": "file_test.mp4", "sha256": " hjkahkjsjkfsjk78687dhjahdajhk " }
  329. //
  330. // 如果要设置上述内容
  331. //
  332. // CreateFormField(w, "meta", "application/json", meta)
  333. func CreateFormField(w *multipart.Writer, fieldName, contentType string, fieldValue []byte) error {
  334. h := make(textproto.MIMEHeader)
  335. h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s";`, fieldName))
  336. h.Set("Content-Type", contentType)
  337. part, err := w.CreatePart(h)
  338. if err != nil {
  339. return err
  340. }
  341. _, err = part.Write(fieldValue)
  342. return err
  343. }
  344. // CreateFormFile 设置form-data中的文件
  345. //
  346. // 示例内容:
  347. //
  348. // Content-Disposition: form-data; name="file"; filename="file_test.mp4";
  349. // Content-Type: video/mp4
  350. //
  351. // pic1 //pic1即为媒体视频的二进制内容
  352. //
  353. // 如果要设置上述内容,则CreateFormFile(w, "file_test.mp4", "video/mp4", pic1)
  354. func CreateFormFile(w *multipart.Writer, filename, contentType string, file []byte) error {
  355. h := make(textproto.MIMEHeader)
  356. h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "file", filename))
  357. h.Set("Content-Type", contentType)
  358. part, err := w.CreatePart(h)
  359. if err != nil {
  360. return err
  361. }
  362. _, err = part.Write(file)
  363. return err
  364. }
  365. // setBody Set Request body from an interface
  366. //
  367. //revive:disable-next-line:cyclomatic 本函数实现需要考虑多种情况,但理解起来并不复杂,进行圈复杂度豁免
  368. func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) {
  369. bodyBuf = &bytes.Buffer{}
  370. switch b := body.(type) {
  371. case string:
  372. _, err = bodyBuf.WriteString(b)
  373. case *string:
  374. _, err = bodyBuf.WriteString(*b)
  375. case []byte:
  376. _, err = bodyBuf.Write(b)
  377. case **os.File:
  378. _, err = bodyBuf.ReadFrom(*b)
  379. case io.Reader:
  380. _, err = bodyBuf.ReadFrom(b)
  381. default:
  382. if regJSONTypeCheck.MatchString(contentType) {
  383. err = json.NewEncoder(bodyBuf).Encode(body)
  384. } else if regXMLTypeCheck.MatchString(contentType) {
  385. err = xml.NewEncoder(bodyBuf).Encode(body)
  386. }
  387. }
  388. if err != nil {
  389. return nil, err
  390. }
  391. if bodyBuf.Len() == 0 {
  392. err = fmt.Errorf("invalid body type %s", contentType)
  393. return nil, err
  394. }
  395. return bodyBuf, nil
  396. }
  397. // contains is a case-insensitive match, finding needle in a haystack
  398. func contains(haystack []string, needle string) bool {
  399. for _, a := range haystack {
  400. if strings.EqualFold(a, needle) {
  401. return true
  402. }
  403. }
  404. return false
  405. }
  406. // SelectHeaderContentType select a content type from the available list.
  407. func SelectHeaderContentType(contentTypes []string) string {
  408. if len(contentTypes) == 0 {
  409. return consts.ApplicationJSON
  410. }
  411. if contains(contentTypes, consts.ApplicationJSON) {
  412. return consts.ApplicationJSON
  413. }
  414. return contentTypes[0] // use the first content type specified in 'consumes'
  415. }
  416. // ParameterToString 将参数转换为字符串,并使用指定分隔符分隔列表参数
  417. func ParameterToString(obj interface{}, collectionFormat string) string {
  418. var delimiter string
  419. switch collectionFormat {
  420. case "pipes":
  421. delimiter = "|"
  422. case "ssv":
  423. delimiter = " "
  424. case "tsv":
  425. delimiter = "\t"
  426. case "csv":
  427. delimiter = ","
  428. }
  429. if reflect.TypeOf(obj).Kind() == reflect.Slice {
  430. return strings.Trim(strings.Replace(fmt.Sprint(obj), " ", delimiter, -1), "[]")
  431. } else if t, ok := obj.(time.Time); ok {
  432. return t.Format(time.RFC3339)
  433. }
  434. return fmt.Sprintf("%v", obj)
  435. }
  436. // ParameterToJSON 将参数转换为 Json 字符串
  437. func ParameterToJSON(obj interface{}) (string, error) {
  438. jsonBuf, err := json.Marshal(obj)
  439. if err != nil {
  440. return "", err
  441. }
  442. return string(jsonBuf), err
  443. }