client.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. package wechat
  2. import (
  3. "crypto/tls"
  4. "encoding/xml"
  5. "errors"
  6. "fmt"
  7. "strings"
  8. "sync"
  9. "github.com/iGoogle-ink/gopay/v2"
  10. )
  11. type Client struct {
  12. AppId string
  13. MchId string
  14. ApiKey string
  15. BaseURL string
  16. CertFile []byte
  17. KeyFile []byte
  18. Pkcs12File []byte
  19. IsProd bool
  20. mu sync.RWMutex
  21. }
  22. // 初始化微信客户端
  23. // appId:应用ID
  24. // mchId:商户ID
  25. // ApiKey:API秘钥值
  26. // IsProd:是否是正式环境
  27. func NewClient(appId, mchId, apiKey string, isProd bool) (client *Client) {
  28. return &Client{
  29. AppId: appId,
  30. MchId: mchId,
  31. ApiKey: apiKey,
  32. IsProd: isProd}
  33. }
  34. // 提交付款码支付
  35. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10&index=1
  36. func (w *Client) Micropay(bm gopay.BodyMap) (wxRsp *MicropayResponse, err error) {
  37. err = bm.CheckEmptyError("nonce_str", "body", "out_trade_no", "total_fee", "spbill_create_ip", "auth_code")
  38. if err != nil {
  39. return nil, err
  40. }
  41. var bs []byte
  42. if w.IsProd {
  43. bs, err = w.doWeChat(bm, wxMicropay, nil)
  44. } else {
  45. bs, err = w.doWeChat(bm, wxSandboxMicropay, nil)
  46. }
  47. if err != nil {
  48. return
  49. }
  50. wxRsp = new(MicropayResponse)
  51. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  52. return nil, fmt.Errorf("xml.Unmarshal(%s):%s", string(bs), err.Error())
  53. }
  54. return
  55. }
  56. // 统一下单
  57. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
  58. func (w *Client) UnifiedOrder(bm gopay.BodyMap) (wxRsp *UnifiedOrderResponse, err error) {
  59. err = bm.CheckEmptyError("nonce_str", "body", "out_trade_no", "total_fee", "spbill_create_ip", "notify_url", "trade_type")
  60. if err != nil {
  61. return nil, err
  62. }
  63. var bs []byte
  64. if w.IsProd {
  65. bs, err = w.doWeChat(bm, wxUnifiedorder, nil)
  66. } else {
  67. bm.Set("total_fee", 101)
  68. bs, err = w.doWeChat(bm, wxSandboxUnifiedorder, nil)
  69. }
  70. if err != nil {
  71. return
  72. }
  73. wxRsp = new(UnifiedOrderResponse)
  74. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  75. return nil, fmt.Errorf("xml.Unmarshal(%s):%s", string(bs), err.Error())
  76. }
  77. return
  78. }
  79. // 查询订单
  80. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_2
  81. func (w *Client) QueryOrder(bm gopay.BodyMap) (wxRsp *QueryOrderResponse, err error) {
  82. err = bm.CheckEmptyError("nonce_str")
  83. if err != nil {
  84. return nil, err
  85. }
  86. if bm.Get("out_trade_no") == gopay.NULL && bm.Get("transaction_id") == gopay.NULL {
  87. return nil, errors.New("out_trade_no and transaction_id are not allowed to be null at the same time")
  88. }
  89. var bs []byte
  90. if w.IsProd {
  91. bs, err = w.doWeChat(bm, wxOrderquery, nil)
  92. } else {
  93. bs, err = w.doWeChat(bm, wxSandboxOrderquery, nil)
  94. }
  95. if err != nil {
  96. return
  97. }
  98. wxRsp = new(QueryOrderResponse)
  99. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  100. return nil, fmt.Errorf("xml.Unmarshal(%s):%s", string(bs), err.Error())
  101. }
  102. return
  103. }
  104. // 关闭订单
  105. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_3
  106. func (w *Client) CloseOrder(bm gopay.BodyMap) (wxRsp *CloseOrderResponse, err error) {
  107. err = bm.CheckEmptyError("nonce_str", "out_trade_no")
  108. if err != nil {
  109. return nil, err
  110. }
  111. var bs []byte
  112. if w.IsProd {
  113. bs, err = w.doWeChat(bm, wxCloseorder, nil)
  114. } else {
  115. bs, err = w.doWeChat(bm, wxSandboxCloseorder, nil)
  116. }
  117. if err != nil {
  118. return
  119. }
  120. wxRsp = new(CloseOrderResponse)
  121. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  122. return nil, fmt.Errorf("xml.Unmarshal(%s):%s", string(bs), err.Error())
  123. }
  124. return
  125. }
  126. // 撤销订单
  127. // 注意:如已使用client.AddCertFilePath()或client.AddCertFileByte()添加过证书,参数certFilePath、keyFilePath、pkcs12FilePath全传空字符串 "",否则,3证书Path均不可空
  128. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_11&index=3
  129. func (w *Client) Reverse(bm gopay.BodyMap, certFilePath, keyFilePath, pkcs12FilePath string) (wxRsp *ReverseResponse, err error) {
  130. err = bm.CheckEmptyError("nonce_str", "out_trade_no")
  131. if err != nil {
  132. return nil, err
  133. }
  134. var (
  135. bs []byte
  136. tlsConfig *tls.Config
  137. )
  138. if w.IsProd {
  139. if tlsConfig, err = w.addCertConfig(certFilePath, keyFilePath, pkcs12FilePath); err != nil {
  140. return nil, err
  141. }
  142. bs, err = w.doWeChat(bm, wxReverse, tlsConfig)
  143. } else {
  144. bs, err = w.doWeChat(bm, wxSandboxReverse, nil)
  145. }
  146. if err != nil {
  147. return
  148. }
  149. wxRsp = new(ReverseResponse)
  150. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  151. return nil, fmt.Errorf("xml.Unmarshal(%s):%s", string(bs), err.Error())
  152. }
  153. return
  154. }
  155. // 申请退款
  156. // 注意:如已使用client.AddCertFilePath()或client.AddCertFileByte()添加过证书,参数certFilePath、keyFilePath、pkcs12FilePath全传空字符串 "",否则,3证书Path均不可空
  157. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
  158. func (w *Client) Refund(bm gopay.BodyMap, certFilePath, keyFilePath, pkcs12FilePath string) (wxRsp *RefundResponse, err error) {
  159. err = bm.CheckEmptyError("nonce_str", "out_refund_no", "total_fee", "refund_fee")
  160. if err != nil {
  161. return nil, err
  162. }
  163. if bm.Get("out_trade_no") == gopay.NULL && bm.Get("transaction_id") == gopay.NULL {
  164. return nil, errors.New("out_trade_no and transaction_id are not allowed to be null at the same time")
  165. }
  166. var (
  167. bs []byte
  168. tlsConfig *tls.Config
  169. )
  170. if w.IsProd {
  171. if tlsConfig, err = w.addCertConfig(certFilePath, keyFilePath, pkcs12FilePath); err != nil {
  172. return nil, err
  173. }
  174. bs, err = w.doWeChat(bm, wxRefund, tlsConfig)
  175. } else {
  176. bs, err = w.doWeChat(bm, wxSandboxRefund, nil)
  177. }
  178. if err != nil {
  179. return
  180. }
  181. wxRsp = new(RefundResponse)
  182. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  183. return nil, fmt.Errorf("xml.Unmarshal(%s):%s", string(bs), err.Error())
  184. }
  185. return
  186. }
  187. // 查询退款
  188. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_5
  189. func (w *Client) QueryRefund(bm gopay.BodyMap) (wxRsp *QueryRefundResponse, err error) {
  190. err = bm.CheckEmptyError("nonce_str")
  191. if err != nil {
  192. return nil, err
  193. }
  194. if bm.Get("refund_id") == gopay.NULL && bm.Get("out_refund_no") == gopay.NULL && bm.Get("transaction_id") == gopay.NULL && bm.Get("out_trade_no") == gopay.NULL {
  195. return nil, errors.New("refund_id, out_refund_no, out_trade_no, transaction_id are not allowed to be null at the same time")
  196. }
  197. var bs []byte
  198. if w.IsProd {
  199. bs, err = w.doWeChat(bm, wxRefundquery, nil)
  200. } else {
  201. bs, err = w.doWeChat(bm, wxSandboxRefundquery, nil)
  202. }
  203. if err != nil {
  204. return
  205. }
  206. wxRsp = new(QueryRefundResponse)
  207. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  208. return nil, fmt.Errorf("xml.Unmarshal(%s):%s", string(bs), err.Error())
  209. }
  210. return
  211. }
  212. // 下载对账单
  213. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_6
  214. func (w *Client) DownloadBill(bm gopay.BodyMap) (wxRsp string, err error) {
  215. err = bm.CheckEmptyError("nonce_str", "bill_date", "bill_type")
  216. if err != nil {
  217. return gopay.NULL, err
  218. }
  219. billType := bm.Get("bill_type")
  220. if billType != "ALL" && billType != "SUCCESS" && billType != "REFUND" && billType != "RECHARGE_REFUND" {
  221. return gopay.NULL, errors.New("bill_type error, please reference: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_6")
  222. }
  223. var bs []byte
  224. if w.IsProd {
  225. bs, err = w.doWeChat(bm, wxDownloadbill, nil)
  226. } else {
  227. bs, err = w.doWeChat(bm, wxSandboxDownloadbill, nil)
  228. }
  229. if err != nil {
  230. return
  231. }
  232. wxRsp = string(bs)
  233. return
  234. }
  235. // 下载资金账单
  236. // 注意:如已使用client.AddCertFilePath()或client.AddCertFileByte()添加过证书,参数certFilePath、keyFilePath、pkcs12FilePath全传空字符串 "",否则,3证书Path均不可空
  237. // 貌似不支持沙箱环境,因为沙箱环境默认需要用MD5签名,但是此接口仅支持HMAC-SHA256签名
  238. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_18&index=7
  239. func (w *Client) DownloadFundFlow(bm gopay.BodyMap, certFilePath, keyFilePath, pkcs12FilePath string) (wxRsp string, err error) {
  240. err = bm.CheckEmptyError("nonce_str", "bill_date", "account_type")
  241. if err != nil {
  242. return gopay.NULL, err
  243. }
  244. accountType := bm.Get("account_type")
  245. if accountType != "Basic" && accountType != "Operation" && accountType != "Fees" {
  246. 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")
  247. }
  248. var (
  249. bs []byte
  250. tlsConfig *tls.Config
  251. )
  252. bm.Set("sign_type", SignType_HMAC_SHA256)
  253. if w.IsProd {
  254. if tlsConfig, err = w.addCertConfig(certFilePath, keyFilePath, pkcs12FilePath); err != nil {
  255. return gopay.NULL, err
  256. }
  257. bs, err = w.doWeChat(bm, wxDownloadfundflow, tlsConfig)
  258. } else {
  259. bs, err = w.doWeChat(bm, wxSandboxDownloadfundflow, nil)
  260. }
  261. if err != nil {
  262. return
  263. }
  264. wxRsp = string(bs)
  265. return
  266. }
  267. // 拉取订单评价数据
  268. // 注意:如已使用client.AddCertFilePath()或client.AddCertFileByte()添加过证书,参数certFilePath、keyFilePath、pkcs12FilePath全传空字符串 "",否则,3证书Path均不可空
  269. // 貌似不支持沙箱环境,因为沙箱环境默认需要用MD5签名,但是此接口仅支持HMAC-SHA256签名
  270. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_17&index=11
  271. func (w *Client) BatchQueryComment(bm gopay.BodyMap, certFilePath, keyFilePath, pkcs12FilePath string) (wxRsp string, err error) {
  272. err = bm.CheckEmptyError("nonce_str", "begin_time", "end_time", "offset")
  273. if err != nil {
  274. return gopay.NULL, err
  275. }
  276. var (
  277. bs []byte
  278. tlsConfig *tls.Config
  279. )
  280. bm.Set("sign_type", SignType_HMAC_SHA256)
  281. if w.IsProd {
  282. if tlsConfig, err = w.addCertConfig(certFilePath, keyFilePath, pkcs12FilePath); err != nil {
  283. return gopay.NULL, err
  284. }
  285. bs, err = w.doWeChat(bm, wxBatchquerycomment, tlsConfig)
  286. } else {
  287. bs, err = w.doWeChat(bm, wxSandboxBatchquerycomment, nil)
  288. }
  289. if err != nil {
  290. return
  291. }
  292. wxRsp = string(bs)
  293. return
  294. }
  295. // 企业向微信用户个人付款
  296. // 注意:如已使用client.AddCertFilePath()或client.AddCertFileByte()添加过证书,参数certFilePath、keyFilePath、pkcs12FilePath全传空字符串 "",否则,3证书Path均不可空
  297. // 注意:此方法未支持沙箱环境,默认正式环境,转账请慎重
  298. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2
  299. func (w *Client) Transfer(bm gopay.BodyMap, certFilePath, keyFilePath, pkcs12FilePath string) (wxRsp *TransfersResponse, err error) {
  300. err = bm.CheckEmptyError("nonce_str", "partner_trade_no", "openid", "check_name", "amount", "desc", "spbill_create_ip")
  301. if err != nil {
  302. return nil, err
  303. }
  304. bm.Set("mch_appid", w.AppId)
  305. bm.Set("mchid", w.MchId)
  306. var (
  307. tlsConfig *tls.Config
  308. url = wxBaseUrlCh + wxTransfers
  309. )
  310. if tlsConfig, err = w.addCertConfig(certFilePath, keyFilePath, pkcs12FilePath); err != nil {
  311. return nil, err
  312. }
  313. bm.Set("sign", getReleaseSign(w.ApiKey, SignType_MD5, bm))
  314. httpClient := gopay.NewHttpClient().SetTLSConfig(tlsConfig).Type(gopay.TypeXML)
  315. if w.BaseURL != gopay.NULL {
  316. w.mu.RLock()
  317. url = w.BaseURL + wxTransfers
  318. w.mu.RUnlock()
  319. }
  320. wxRsp = new(TransfersResponse)
  321. res, errs := httpClient.Post(url).SendString(generateXml(bm)).EndStruct(wxRsp)
  322. if len(errs) > 0 {
  323. return nil, errs[0]
  324. }
  325. if res.StatusCode != 200 {
  326. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  327. }
  328. return wxRsp, nil
  329. }
  330. // 公众号纯签约(未完成)
  331. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/pap.php?chapter=18_1&index=1
  332. func (w *Client) EntrustPublic(bm gopay.BodyMap) (bs []byte, err error) {
  333. bs, err = w.doWeChat(bm, wxEntrustPublic, nil)
  334. return nil, nil
  335. }
  336. // 向微信发送请求
  337. func (w *Client) doWeChat(bm gopay.BodyMap, path string, tlsConfig *tls.Config) (bs []byte, err error) {
  338. var url = wxBaseUrlCh + path
  339. bm.Set("appid", w.AppId)
  340. bm.Set("mch_id", w.MchId)
  341. if bm.Get("sign") == gopay.NULL {
  342. var sign string
  343. if !w.IsProd {
  344. bm.Set("sign_type", SignType_MD5)
  345. sign, err = getSignBoxSign(w.MchId, w.ApiKey, bm)
  346. if err != nil {
  347. return nil, err
  348. }
  349. } else {
  350. sign = getReleaseSign(w.ApiKey, bm.Get("sign_type"), bm)
  351. }
  352. bm.Set("sign", sign)
  353. }
  354. httpClient := gopay.NewHttpClient()
  355. if w.IsProd && tlsConfig != nil {
  356. httpClient.SetTLSConfig(tlsConfig)
  357. }
  358. if w.BaseURL != gopay.NULL {
  359. w.mu.RLock()
  360. url = w.BaseURL + path
  361. w.mu.RUnlock()
  362. }
  363. res, bs, errs := httpClient.Type(gopay.TypeXML).Post(url).SendString(generateXml(bm)).EndBytes()
  364. if len(errs) > 0 {
  365. return nil, errs[0]
  366. }
  367. if res.StatusCode != 200 {
  368. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  369. }
  370. if strings.Contains(string(bs), "HTML") {
  371. return nil, errors.New(string(bs))
  372. }
  373. return bs, nil
  374. }