蛋蛋星球-制度模式
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

436 lines
13 KiB

  1. package wechat
  2. import (
  3. "context"
  4. "crypto/hmac"
  5. "crypto/md5"
  6. "crypto/sha256"
  7. "crypto/tls"
  8. "encoding/xml"
  9. "errors"
  10. "fmt"
  11. "hash"
  12. "strings"
  13. "sync"
  14. "github.com/go-pay/gopay"
  15. "github.com/go-pay/gopay/pkg/xhttp"
  16. "github.com/go-pay/xlog"
  17. )
  18. type Client struct {
  19. AppId string
  20. MchId string
  21. ApiKey string
  22. BaseURL string
  23. IsProd bool
  24. DebugSwitch gopay.DebugSwitch
  25. logger xlog.XLogger
  26. mu sync.RWMutex
  27. sha256Hash hash.Hash
  28. md5Hash hash.Hash
  29. hc *xhttp.Client
  30. tlsHc *xhttp.Client
  31. }
  32. // 初始化微信客户端 V2
  33. // appId:应用ID
  34. // mchId:商户ID
  35. // ApiKey:API秘钥值
  36. // IsProd:是否是正式环境
  37. func NewClient(appId, mchId, apiKey string, isProd bool) (client *Client) {
  38. logger := xlog.NewLogger()
  39. logger.SetLevel(xlog.DebugLevel)
  40. return &Client{
  41. AppId: appId,
  42. MchId: mchId,
  43. ApiKey: apiKey,
  44. IsProd: isProd,
  45. DebugSwitch: gopay.DebugOff,
  46. logger: logger,
  47. sha256Hash: hmac.New(sha256.New, []byte(apiKey)),
  48. md5Hash: md5.New(),
  49. hc: xhttp.NewClient(),
  50. tlsHc: xhttp.NewClient(),
  51. }
  52. }
  53. // SetBodySize 设置http response body size(MB)
  54. func (w *Client) SetBodySize(sizeMB int) {
  55. if sizeMB > 0 {
  56. w.hc.SetBodySize(sizeMB)
  57. }
  58. }
  59. // SetHttpClient 设置自定义的xhttp.Client
  60. func (w *Client) SetHttpClient(client *xhttp.Client) {
  61. if client != nil {
  62. w.hc = client
  63. }
  64. }
  65. // SetTLSHttpClient 设置自定义的xhttp.Client
  66. func (w *Client) SetTLSHttpClient(client *xhttp.Client) {
  67. if client != nil {
  68. w.tlsHc = client
  69. }
  70. }
  71. func (w *Client) SetLogger(logger xlog.XLogger) {
  72. if logger != nil {
  73. w.logger = logger
  74. }
  75. }
  76. // 向微信发送Post请求,对于本库未提供的微信API,可自行实现,通过此方法发送请求
  77. // bm:请求参数的BodyMap
  78. // path:接口地址去掉baseURL的path,例如:url为https://api.mch.weixin.qq.com/pay/micropay,只需传 pay/micropay
  79. // tlsConfig:tls配置,如无需证书请求,传nil
  80. func (w *Client) PostWeChatAPISelf(ctx context.Context, bm gopay.BodyMap, path string, tlsConfig *tls.Config) (bs []byte, err error) {
  81. return w.doProdPostSelf(ctx, bm, path, tlsConfig)
  82. }
  83. // 授权码查询openid(正式)
  84. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/wxpay_v2/open/chapter4_8.shtml
  85. func (w *Client) AuthCodeToOpenId(ctx context.Context, bm gopay.BodyMap) (wxRsp *AuthCodeToOpenIdResponse, err error) {
  86. err = bm.CheckEmptyError("nonce_str", "auth_code")
  87. if err != nil {
  88. return nil, err
  89. }
  90. bs, err := w.doProdPost(ctx, bm, authCodeToOpenid)
  91. if err != nil {
  92. return nil, err
  93. }
  94. wxRsp = new(AuthCodeToOpenIdResponse)
  95. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  96. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  97. }
  98. return wxRsp, nil
  99. }
  100. // 下载对账单
  101. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/wxpay_v2/open/chapter3_6.shtml
  102. func (w *Client) DownloadBill(ctx context.Context, bm gopay.BodyMap) (wxRsp string, err error) {
  103. err = bm.CheckEmptyError("nonce_str", "bill_date", "bill_type")
  104. if err != nil {
  105. return gopay.NULL, err
  106. }
  107. billType := bm.GetString("bill_type")
  108. if billType != "ALL" && billType != "SUCCESS" && billType != "REFUND" && billType != "RECHARGE_REFUND" {
  109. return gopay.NULL, errors.New("bill_type error, please reference: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_6")
  110. }
  111. var bs []byte
  112. if w.IsProd {
  113. bs, err = w.doProdPost(ctx, bm, downloadBill)
  114. } else {
  115. bs, err = w.doSanBoxPost(ctx, bm, sandboxDownloadBill)
  116. }
  117. if err != nil {
  118. return gopay.NULL, err
  119. }
  120. return string(bs), nil
  121. }
  122. // 下载资金账单(正式)
  123. // 注意:请在初始化client时,调用 client 添加证书的相关方法添加证书
  124. // 不支持沙箱环境,因为沙箱环境默认需要用MD5签名,但是此接口仅支持HMAC-SHA256签名
  125. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/wxpay_v2/open/chapter3_7.shtml
  126. func (w *Client) DownloadFundFlow(ctx context.Context, bm gopay.BodyMap) (wxRsp string, err error) {
  127. err = bm.CheckEmptyError("nonce_str", "bill_date", "account_type")
  128. if err != nil {
  129. return gopay.NULL, err
  130. }
  131. accountType := bm.GetString("account_type")
  132. if accountType != "Basic" && accountType != "Operation" && accountType != "Fees" {
  133. return gopay.NULL, errors.New("account_type error, please reference: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_18&index=7")
  134. }
  135. bm.Set("sign_type", SignType_HMAC_SHA256)
  136. bs, err := w.doProdPostTLS(ctx, bm, downloadFundFlow)
  137. if err != nil {
  138. return gopay.NULL, err
  139. }
  140. wxRsp = string(bs)
  141. return
  142. }
  143. // 交易保障
  144. // 文档地址:(JSAPI)https://pay.weixin.qq.com/wiki/doc/api/wxpay_v2/open/chapter3_9.shtml
  145. // 文档地址:(付款码)https://pay.weixin.qq.com/wiki/doc/api/wxpay_v2/open/chapter4_9.shtml
  146. // 文档地址:(Native)https://pay.weixin.qq.com/wiki/doc/api/wxpay_v2/open/chapter6_9.shtml
  147. // 文档地址:(APP)https://pay.weixin.qq.com/wiki/doc/api/wxpay_v2/open/chapter7_9.shtml
  148. // 文档地址:(H5)https://pay.weixin.qq.com/wiki/doc/api/wxpay_v2/open/chapter8_9.shtml
  149. // 文档地址:(微信小程序)https://pay.weixin.qq.com/wiki/doc/api/wxpay_v2/open/chapter5_9.shtml
  150. func (w *Client) Report(ctx context.Context, bm gopay.BodyMap) (wxRsp *ReportResponse, err error) {
  151. err = bm.CheckEmptyError("nonce_str", "interface_url", "execute_time", "return_code", "return_msg", "result_code", "user_ip")
  152. if err != nil {
  153. return nil, err
  154. }
  155. var bs []byte
  156. if w.IsProd {
  157. bs, err = w.doProdPost(ctx, bm, report)
  158. } else {
  159. bs, err = w.doSanBoxPost(ctx, bm, sandboxReport)
  160. }
  161. if err != nil {
  162. return nil, err
  163. }
  164. wxRsp = new(ReportResponse)
  165. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  166. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  167. }
  168. return wxRsp, nil
  169. }
  170. // 拉取订单评价数据(正式)
  171. // 注意:请在初始化client时,调用 client 添加证书的相关方法添加证书
  172. // 不支持沙箱环境,因为沙箱环境默认需要用MD5签名,但是此接口仅支持HMAC-SHA256签名
  173. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/wxpay_v2/open/chapter3_11.shtml
  174. func (w *Client) BatchQueryComment(ctx context.Context, bm gopay.BodyMap) (wxRsp string, err error) {
  175. err = bm.CheckEmptyError("nonce_str", "begin_time", "end_time", "offset")
  176. if err != nil {
  177. return gopay.NULL, err
  178. }
  179. bm.Set("sign_type", SignType_HMAC_SHA256)
  180. bs, err := w.doProdPostTLS(ctx, bm, batchQueryComment)
  181. if err != nil {
  182. return gopay.NULL, err
  183. }
  184. return string(bs), nil
  185. }
  186. // doSanBoxPost sanbox环境post请求
  187. func (w *Client) doSanBoxPost(ctx context.Context, bm gopay.BodyMap, path string) (bs []byte, err error) {
  188. var url = baseUrlCh + path
  189. bm.Set("appid", w.AppId)
  190. bm.Set("mch_id", w.MchId)
  191. if bm.GetString("sign") == gopay.NULL {
  192. bm.Set("sign_type", SignType_MD5)
  193. sign, err := w.getSandBoxSign(ctx, w.MchId, w.ApiKey, bm)
  194. if err != nil {
  195. return nil, err
  196. }
  197. bm.Set("sign", sign)
  198. }
  199. if w.BaseURL != gopay.NULL {
  200. url = w.BaseURL + path
  201. }
  202. req := GenerateXml(bm)
  203. if w.DebugSwitch == gopay.DebugOn {
  204. w.logger.Debugf("Wechat_Request: %s", req)
  205. }
  206. res, bs, err := w.hc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  207. if err != nil {
  208. return nil, err
  209. }
  210. if w.DebugSwitch == gopay.DebugOn {
  211. w.logger.Debugf("Wechat_Response: %d, %s", res.StatusCode, string(bs))
  212. }
  213. if res.StatusCode != 200 {
  214. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  215. }
  216. if strings.Contains(string(bs), "HTML") || strings.Contains(string(bs), "html") {
  217. return nil, errors.New(string(bs))
  218. }
  219. return bs, nil
  220. }
  221. // Post请求、正式
  222. func (w *Client) doProdPostSelf(ctx context.Context, bm gopay.BodyMap, path string, tlsConfig *tls.Config) (bs []byte, err error) {
  223. var url = baseUrlCh + path
  224. if bm.GetString("appid") == gopay.NULL {
  225. bm.Set("appid", w.AppId)
  226. }
  227. if bm.GetString("mch_id") == gopay.NULL {
  228. bm.Set("mch_id", w.MchId)
  229. }
  230. if bm.GetString("sign") == gopay.NULL {
  231. sign := w.getReleaseSign(w.ApiKey, bm.GetString("sign_type"), bm)
  232. bm.Set("sign", sign)
  233. }
  234. if w.BaseURL != gopay.NULL {
  235. url = w.BaseURL + path
  236. }
  237. req := GenerateXml(bm)
  238. if w.DebugSwitch == gopay.DebugOn {
  239. w.logger.Debugf("Wechat_Request: %s", req)
  240. }
  241. httpClient := xhttp.NewClient()
  242. if w.IsProd && tlsConfig != nil {
  243. httpClient.SetTLSConfig(tlsConfig)
  244. }
  245. res, bs, err := httpClient.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  246. if err != nil {
  247. return nil, err
  248. }
  249. if w.DebugSwitch == gopay.DebugOn {
  250. w.logger.Debugf("Wechat_Response: %d, %s", res.StatusCode, string(bs))
  251. }
  252. if res.StatusCode != 200 {
  253. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  254. }
  255. if strings.Contains(string(bs), "HTML") || strings.Contains(string(bs), "html") {
  256. return nil, errors.New(string(bs))
  257. }
  258. return bs, nil
  259. }
  260. // Post请求、正式
  261. func (w *Client) doProdPost(ctx context.Context, bm gopay.BodyMap, path string) (bs []byte, err error) {
  262. var url = baseUrlCh + path
  263. if bm.GetString("appid") == gopay.NULL {
  264. bm.Set("appid", w.AppId)
  265. }
  266. if bm.GetString("mch_id") == gopay.NULL {
  267. bm.Set("mch_id", w.MchId)
  268. }
  269. if bm.GetString("sign") == gopay.NULL {
  270. sign := w.getReleaseSign(w.ApiKey, bm.GetString("sign_type"), bm)
  271. bm.Set("sign", sign)
  272. }
  273. if w.BaseURL != gopay.NULL {
  274. url = w.BaseURL + path
  275. }
  276. req := GenerateXml(bm)
  277. if w.DebugSwitch == gopay.DebugOn {
  278. w.logger.Debugf("Wechat_Request: %s", req)
  279. }
  280. res, bs, err := w.hc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  281. if err != nil {
  282. return nil, err
  283. }
  284. if w.DebugSwitch == gopay.DebugOn {
  285. w.logger.Debugf("Wechat_Response: %d, %s", res.StatusCode, string(bs))
  286. }
  287. if res.StatusCode != 200 {
  288. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  289. }
  290. if strings.Contains(string(bs), "<HTML") || strings.Contains(string(bs), "<html") {
  291. return nil, errors.New(string(bs))
  292. }
  293. return bs, nil
  294. }
  295. func (w *Client) doProdPostTLS(ctx context.Context, bm gopay.BodyMap, path string) (bs []byte, err error) {
  296. var url = baseUrlCh + path
  297. if bm.GetString("appid") == gopay.NULL {
  298. bm.Set("appid", w.AppId)
  299. }
  300. if bm.GetString("mch_id") == gopay.NULL {
  301. bm.Set("mch_id", w.MchId)
  302. }
  303. if bm.GetString("sign") == gopay.NULL {
  304. sign := w.getReleaseSign(w.ApiKey, bm.GetString("sign_type"), bm)
  305. bm.Set("sign", sign)
  306. }
  307. if w.BaseURL != gopay.NULL {
  308. url = w.BaseURL + path
  309. }
  310. req := GenerateXml(bm)
  311. if w.DebugSwitch == gopay.DebugOn {
  312. w.logger.Debugf("Wechat_Request: %s", req)
  313. }
  314. res, bs, err := w.tlsHc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  315. if err != nil {
  316. return nil, err
  317. }
  318. if w.DebugSwitch == gopay.DebugOn {
  319. w.logger.Debugf("Wechat_Response: %d, %s", res.StatusCode, string(bs))
  320. }
  321. if res.StatusCode != 200 {
  322. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  323. }
  324. if strings.Contains(string(bs), "<HTML") || strings.Contains(string(bs), "<html") {
  325. return nil, errors.New(string(bs))
  326. }
  327. return bs, nil
  328. }
  329. func (w *Client) doProdPostPure(ctx context.Context, bm gopay.BodyMap, path string) (bs []byte, err error) {
  330. var url = baseUrlCh + path
  331. if w.BaseURL != gopay.NULL {
  332. url = w.BaseURL + path
  333. }
  334. req := GenerateXml(bm)
  335. if w.DebugSwitch == gopay.DebugOn {
  336. w.logger.Debugf("Wechat_Request: %s", req)
  337. }
  338. res, bs, err := w.hc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  339. if err != nil {
  340. return nil, err
  341. }
  342. if w.DebugSwitch == gopay.DebugOn {
  343. w.logger.Debugf("Wechat_Response: %d, %s", res.StatusCode, string(bs))
  344. }
  345. if res.StatusCode != 200 {
  346. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  347. }
  348. if strings.Contains(string(bs), "<HTML") || strings.Contains(string(bs), "<html") {
  349. return nil, errors.New(string(bs))
  350. }
  351. return bs, nil
  352. }
  353. func (w *Client) doProdPostPureTLS(ctx context.Context, bm gopay.BodyMap, path string) (bs []byte, err error) {
  354. var url = baseUrlCh + path
  355. if w.BaseURL != gopay.NULL {
  356. url = w.BaseURL + path
  357. }
  358. req := GenerateXml(bm)
  359. if w.DebugSwitch == gopay.DebugOn {
  360. w.logger.Debugf("Wechat_Request: %s", req)
  361. }
  362. res, bs, err := w.tlsHc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  363. if err != nil {
  364. return nil, err
  365. }
  366. if w.DebugSwitch == gopay.DebugOn {
  367. w.logger.Debugf("Wechat_Response: %d, %s", res.StatusCode, string(bs))
  368. }
  369. if res.StatusCode != 200 {
  370. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  371. }
  372. if strings.Contains(string(bs), "<HTML") || strings.Contains(string(bs), "<html") {
  373. return nil, errors.New(string(bs))
  374. }
  375. return bs, nil
  376. }
  377. // Get请求、正式
  378. func (w *Client) doProdGet(ctx context.Context, bm gopay.BodyMap, path, signType string) (bs []byte, err error) {
  379. var url = baseUrlCh + path
  380. if bm.GetString("appid") == gopay.NULL {
  381. bm.Set("appid", w.AppId)
  382. }
  383. if bm.GetString("mch_id") == gopay.NULL {
  384. bm.Set("mch_id", w.MchId)
  385. }
  386. bm.Remove("sign")
  387. sign := w.getReleaseSign(w.ApiKey, signType, bm)
  388. bm.Set("sign", sign)
  389. if w.BaseURL != gopay.NULL {
  390. url = w.BaseURL + path
  391. }
  392. if w.DebugSwitch == gopay.DebugOn {
  393. w.logger.Debugf("Wechat_Request: %s", bm.JsonBody())
  394. }
  395. param := bm.EncodeURLParams()
  396. uri := url + "?" + param
  397. res, bs, err := w.hc.Req(xhttp.TypeXML).Get(uri).EndBytes(ctx)
  398. if err != nil {
  399. return nil, err
  400. }
  401. if w.DebugSwitch == gopay.DebugOn {
  402. w.logger.Debugf("Wechat_Response: %d, %s", res.StatusCode, string(bs))
  403. }
  404. if res.StatusCode != 200 {
  405. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  406. }
  407. if strings.Contains(string(bs), "<HTML") || strings.Contains(string(bs), "<html") {
  408. return nil, errors.New(string(bs))
  409. }
  410. return bs, nil
  411. }