蛋蛋星球-制度模式
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.

444 line
18 KiB

  1. package wechat
  2. import (
  3. "context"
  4. "encoding/xml"
  5. "errors"
  6. "fmt"
  7. "github.com/go-pay/gopay"
  8. "github.com/go-pay/gopay/pkg/xhttp"
  9. )
  10. // 企业付款(企业向微信用户个人付款)
  11. // 注意:请在初始化client时,调用 client 添加证书的相关方法添加证书
  12. // 注意:此方法未支持沙箱环境,默认正式环境,转账请慎重
  13. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2
  14. func (w *Client) Transfer(ctx context.Context, bm gopay.BodyMap) (wxRsp *TransfersResponse, err error) {
  15. if err = bm.CheckEmptyError("nonce_str", "partner_trade_no", "openid", "check_name", "amount", "desc", "spbill_create_ip"); err != nil {
  16. return nil, err
  17. }
  18. bm.Set("mch_appid", w.AppId)
  19. bm.Set("mchid", w.MchId)
  20. var (
  21. url = baseUrlCh + transfers
  22. )
  23. bm.Set("sign", w.getReleaseSign(w.ApiKey, SignType_MD5, bm))
  24. if w.BaseURL != gopay.NULL {
  25. w.mu.RLock()
  26. url = w.BaseURL + transfers
  27. w.mu.RUnlock()
  28. }
  29. req := GenerateXml(bm)
  30. if w.DebugSwitch == gopay.DebugOn {
  31. w.logger.Debugf("Wechat_Request: %s", req)
  32. }
  33. res, bs, err := w.tlsHc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  34. if err != nil {
  35. return nil, err
  36. }
  37. if w.DebugSwitch == gopay.DebugOn {
  38. w.logger.Debugf("Wechat_Response: %d, %s", res.StatusCode, string(bs))
  39. }
  40. if res.StatusCode != 200 {
  41. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  42. }
  43. wxRsp = new(TransfersResponse)
  44. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  45. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  46. }
  47. return wxRsp, nil
  48. }
  49. // 查询企业付款
  50. // 注意:请在初始化client时,调用 client 添加证书的相关方法添加证书
  51. // 注意:此方法未支持沙箱环境,默认正式环境
  52. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_3
  53. func (w *Client) GetTransferInfo(ctx context.Context, bm gopay.BodyMap) (wxRsp *TransfersInfoResponse, err error) {
  54. if err = bm.CheckEmptyError("nonce_str", "partner_trade_no"); err != nil {
  55. return nil, err
  56. }
  57. bm.Set("appid", w.AppId)
  58. bm.Set("mch_id", w.MchId)
  59. var (
  60. url = baseUrlCh + getTransferInfo
  61. )
  62. bm.Set("sign", w.getReleaseSign(w.ApiKey, SignType_MD5, bm))
  63. if w.BaseURL != gopay.NULL {
  64. w.mu.RLock()
  65. url = w.BaseURL + getTransferInfo
  66. w.mu.RUnlock()
  67. }
  68. req := GenerateXml(bm)
  69. if w.DebugSwitch == gopay.DebugOn {
  70. w.logger.Debugf("Wechat_Request: %s", req)
  71. }
  72. res, bs, err := w.tlsHc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  73. if err != nil {
  74. return nil, err
  75. }
  76. if w.DebugSwitch == gopay.DebugOn {
  77. w.logger.Debugf("Wechat_Response: %d, %s", res.StatusCode, string(bs))
  78. }
  79. if res.StatusCode != 200 {
  80. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  81. }
  82. wxRsp = new(TransfersInfoResponse)
  83. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  84. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  85. }
  86. return wxRsp, nil
  87. }
  88. // 企业付款到银行卡API(正式)
  89. // 注意:请在初始化client时,调用 client 添加证书的相关方法添加证书
  90. // 注意:此方法未支持沙箱环境,默认正式环境,转账请慎重
  91. // 注意:enc_bank_no、enc_true_name 两参数,开发者需自行获取RSA公钥,加密后再 Set 到 BodyMap,参考 client_test.go 里的 TestClient_PayBank() 方法
  92. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=24_2
  93. // RSA加密文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=24_7
  94. // 银行编码查看地址:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=24_4&index=5
  95. func (w *Client) PayBank(ctx context.Context, bm gopay.BodyMap) (wxRsp *PayBankResponse, err error) {
  96. if err = bm.CheckEmptyError("partner_trade_no", "nonce_str", "enc_bank_no", "enc_true_name", "bank_code", "amount"); err != nil {
  97. return nil, err
  98. }
  99. bm.Set("mch_id", w.MchId)
  100. var (
  101. url = baseUrlCh + payBank
  102. )
  103. bm.Set("sign", w.getReleaseSign(w.ApiKey, SignType_MD5, bm))
  104. if w.BaseURL != gopay.NULL {
  105. w.mu.RLock()
  106. url = w.BaseURL + payBank
  107. w.mu.RUnlock()
  108. }
  109. req := GenerateXml(bm)
  110. if w.DebugSwitch == gopay.DebugOn {
  111. w.logger.Debugf("Wechat_Request: %s", req)
  112. }
  113. res, bs, err := w.tlsHc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  114. if err != nil {
  115. return nil, err
  116. }
  117. if w.DebugSwitch == gopay.DebugOn {
  118. w.logger.Debugf("Wechat_Response: %d, %s", res.StatusCode, string(bs))
  119. }
  120. if res.StatusCode != 200 {
  121. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  122. }
  123. wxRsp = new(PayBankResponse)
  124. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  125. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  126. }
  127. return wxRsp, nil
  128. }
  129. // 查询企业付款到银行卡API(正式)
  130. // 注意:请在初始化client时,调用 client 添加证书的相关方法添加证书
  131. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=24_3
  132. func (w *Client) QueryBank(ctx context.Context, bm gopay.BodyMap) (wxRsp *QueryBankResponse, err error) {
  133. if err = bm.CheckEmptyError("nonce_str", "partner_trade_no"); err != nil {
  134. return nil, err
  135. }
  136. bm.Set("mch_id", w.MchId)
  137. var (
  138. url = baseUrlCh + queryBank
  139. )
  140. bm.Set("sign", w.getReleaseSign(w.ApiKey, SignType_MD5, bm))
  141. if w.BaseURL != gopay.NULL {
  142. w.mu.RLock()
  143. url = w.BaseURL + queryBank
  144. w.mu.RUnlock()
  145. }
  146. req := GenerateXml(bm)
  147. if w.DebugSwitch == gopay.DebugOn {
  148. w.logger.Debugf("Wechat_Request: %s", req)
  149. }
  150. res, bs, err := w.tlsHc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  151. if err != nil {
  152. return nil, err
  153. }
  154. if w.DebugSwitch == gopay.DebugOn {
  155. w.logger.Debugf("Wechat_Response: %d, %s", res.StatusCode, string(bs))
  156. }
  157. if res.StatusCode != 200 {
  158. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  159. }
  160. wxRsp = new(QueryBankResponse)
  161. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  162. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  163. }
  164. return wxRsp, nil
  165. }
  166. // 获取RSA加密公钥API(正式)
  167. // 注意:请在初始化client时,调用 client 添加证书的相关方法添加证书
  168. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=24_7&index=4
  169. func (w *Client) GetRSAPublicKey(ctx context.Context, bm gopay.BodyMap) (wxRsp *RSAPublicKeyResponse, err error) {
  170. if err = bm.CheckEmptyError("nonce_str", "sign_type"); err != nil {
  171. return nil, err
  172. }
  173. bm.Set("mch_id", w.MchId)
  174. var (
  175. url = getPublicKey
  176. )
  177. bm.Set("sign", w.getReleaseSign(w.ApiKey, bm.GetString("sign_type"), bm))
  178. req := GenerateXml(bm)
  179. if w.DebugSwitch == gopay.DebugOn {
  180. w.logger.Debugf("Wechat_Request: %s", req)
  181. }
  182. res, bs, err := w.tlsHc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  183. if err != nil {
  184. return nil, err
  185. }
  186. if w.DebugSwitch == gopay.DebugOn {
  187. w.logger.Debugf("Wechat_Response: %d, %s", res.StatusCode, string(bs))
  188. }
  189. if res.StatusCode != 200 {
  190. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  191. }
  192. wxRsp = new(RSAPublicKeyResponse)
  193. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  194. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  195. }
  196. return wxRsp, nil
  197. }
  198. // 请求单次分账
  199. // 单次分账请求按照传入的分账接收方账号和资金进行分账,
  200. // 同时会将订单剩余的待分账金额解冻给本商户。
  201. // 故操作成功后,订单不能再进行分账,也不能进行分账完结。
  202. // 注意:请在初始化client时,调用 client 添加证书的相关方法添加证书
  203. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=27_1&index=1
  204. func (w *Client) ProfitSharing(ctx context.Context, bm gopay.BodyMap) (wxRsp *ProfitSharingResponse, err error) {
  205. return w.profitSharing(ctx, bm, profitSharing)
  206. }
  207. // 请求多次分账
  208. // 微信订单支付成功后,商户发起分账请求,将结算后的钱分到分账接收方。多次分账请求仅会按照传入的分账接收方进行分账,不会对剩余的金额进行任何操作。
  209. // 故操作成功后,在待分账金额不等于零时,订单依旧能够再次进行分账。
  210. // 多次分账,可以将本商户作为分账接收方直接传入,实现释放资金给本商户的功能
  211. // 对同一笔订单最多能发起20次多次分账请求
  212. // 注意:请在初始化client时,调用 client 添加证书的相关方法添加证书
  213. // 文档地址:https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=27_1&index=1
  214. func (w *Client) MultiProfitSharing(ctx context.Context, bm gopay.BodyMap) (wxRsp *ProfitSharingResponse, err error) {
  215. return w.profitSharing(ctx, bm, multiProfitSharing)
  216. }
  217. func (w *Client) profitSharing(ctx context.Context, bm gopay.BodyMap, uri string) (wxRsp *ProfitSharingResponse, err error) {
  218. err = bm.CheckEmptyError("nonce_str", "transaction_id", "out_order_no", "receivers")
  219. if err != nil {
  220. return nil, err
  221. }
  222. // 设置签名类型,官方文档此接口只支持 HMAC_SHA256
  223. bm.Set("sign_type", SignType_HMAC_SHA256)
  224. bs, err := w.doProdPostTLS(ctx, bm, uri)
  225. if err != nil {
  226. return nil, err
  227. }
  228. wxRsp = new(ProfitSharingResponse)
  229. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  230. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  231. }
  232. return wxRsp, nil
  233. }
  234. // 查询分账结果
  235. // 发起分账请求后,可调用此接口查询分账结果;发起分账完结请求后,可调用此接口查询分账完结的执行结果。
  236. // 微信文档:https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=27_2&index=3
  237. func (w *Client) ProfitSharingQuery(ctx context.Context, bm gopay.BodyMap) (wxRsp *ProfitSharingQueryResponse, err error) {
  238. err = bm.CheckEmptyError("transaction_id", "out_order_no", "nonce_str")
  239. if err != nil {
  240. return nil, err
  241. }
  242. // 设置签名类型,官方文档此接口只支持 HMAC_SHA256
  243. bm.Set("sign_type", SignType_HMAC_SHA256)
  244. bm.Set("mch_id", w.MchId)
  245. if bm.GetString("sign") == gopay.NULL {
  246. sign := w.getReleaseSign(w.ApiKey, bm.GetString("sign_type"), bm)
  247. bm.Set("sign", sign)
  248. }
  249. bs, err := w.doProdPostPure(ctx, bm, profitSharingQuery)
  250. if err != nil {
  251. return nil, err
  252. }
  253. wxRsp = new(ProfitSharingQueryResponse)
  254. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  255. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  256. }
  257. return wxRsp, nil
  258. }
  259. // 添加分账接收方
  260. // 商户发起添加分账接收方请求,后续可通过发起分账请求将结算后的钱分到该分账接收方。
  261. // 微信文档:https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=27_3&index=4
  262. func (w *Client) ProfitSharingAddReceiver(ctx context.Context, bm gopay.BodyMap) (wxRsp *ProfitSharingAddReceiverResponse, err error) {
  263. err = bm.CheckEmptyError("nonce_str", "receiver")
  264. if err != nil {
  265. return nil, err
  266. }
  267. // 设置签名类型,官方文档此接口只支持 HMAC_SHA256
  268. bm.Set("sign_type", SignType_HMAC_SHA256)
  269. bs, err := w.doProdPost(ctx, bm, profitSharingAddReceiver)
  270. if err != nil {
  271. return nil, err
  272. }
  273. wxRsp = new(ProfitSharingAddReceiverResponse)
  274. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  275. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  276. }
  277. return wxRsp, nil
  278. }
  279. // 删除分账接收方
  280. // 商户发起删除分账接收方请求,删除后不支持将结算后的钱分到该分账接收方
  281. // 微信文档:https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=27_4&index=5
  282. func (w *Client) ProfitSharingRemoveReceiver(ctx context.Context, bm gopay.BodyMap) (wxRsp *ProfitSharingAddReceiverResponse, err error) {
  283. err = bm.CheckEmptyError("nonce_str", "receiver")
  284. if err != nil {
  285. return nil, err
  286. }
  287. // 设置签名类型,官方文档此接口只支持 HMAC_SHA256
  288. bm.Set("sign_type", SignType_HMAC_SHA256)
  289. bs, err := w.doProdPost(ctx, bm, profitSharingRemoveReceiver)
  290. if err != nil {
  291. return nil, err
  292. }
  293. wxRsp = new(ProfitSharingAddReceiverResponse)
  294. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  295. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  296. }
  297. return wxRsp, nil
  298. }
  299. // 完结分账
  300. // 1、不需要进行分账的订单,可直接调用本接口将订单的金额全部解冻给本商户
  301. // 2、调用多次分账接口后,需要解冻剩余资金时,调用本接口将剩余的分账金额全部解冻给特约商户
  302. // 3、已调用请求单次分账后,剩余待分账金额为零,不需要再调用此接口。
  303. // 注意:请在初始化client时,调用 client 添加证书的相关方法添加证书
  304. // 微信文档:https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=27_5&index=6
  305. func (w *Client) ProfitSharingFinish(ctx context.Context, bm gopay.BodyMap) (wxRsp *ProfitSharingResponse, err error) {
  306. err = bm.CheckEmptyError("nonce_str", "transaction_id", "out_order_no", "description")
  307. if err != nil {
  308. return nil, err
  309. }
  310. // 设置签名类型,官方文档此接口只支持 HMAC_SHA256
  311. bm.Set("sign_type", SignType_HMAC_SHA256)
  312. bs, err := w.doProdPostTLS(ctx, bm, profitSharingFinish)
  313. if err != nil {
  314. return nil, err
  315. }
  316. wxRsp = new(ProfitSharingResponse)
  317. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  318. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  319. }
  320. return wxRsp, nil
  321. }
  322. // 服务商可通过调用此接口查询订单剩余待分金额
  323. // 接口频率:30QPS
  324. // 微信文档:https://pay.weixin.qq.com/wiki/doc/api/allocation_sl.php?chapter=25_10&index=7
  325. func (w *Client) ProfitSharingOrderAmountQuery(ctx context.Context, bm gopay.BodyMap) (wxRsp *ProfitSharingOrderAmountQueryResponse, err error) {
  326. err = bm.CheckEmptyError("mch_id", "transaction_id", "nonce_str")
  327. if err != nil {
  328. return nil, err
  329. }
  330. // 设置签名类型,官方文档此接口只支持 HMAC_SHA256
  331. bm.Set("sign_type", SignType_HMAC_SHA256)
  332. bs, err := w.doProdPostTLS(ctx, bm, profitSharingOrderAmountQuery)
  333. if err != nil {
  334. return nil, err
  335. }
  336. wxRsp = new(ProfitSharingOrderAmountQueryResponse)
  337. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  338. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  339. }
  340. return wxRsp, nil
  341. }
  342. // 服务商可以查询子商户设置的允许服务商分账的最大比例
  343. // 接口频率:30QPS
  344. // 微信文档:https://pay.weixin.qq.com/wiki/doc/api/allocation_sl.php?chapter=25_10&index=7
  345. func (w *Client) ProfitSharingMerchantRatioQuery(ctx context.Context, bm gopay.BodyMap) (wxRsp *ProfitSharingMerchantRatioQuery, err error) {
  346. err = bm.CheckEmptyError("mch_id", "nonce_str")
  347. if err != nil {
  348. return nil, err
  349. }
  350. if (bm.GetString("sub_mch_id") == gopay.NULL) && (bm.GetString("brand_mch_id") == gopay.NULL) {
  351. return nil, errors.New("param sub_mch_id and brand_mch_id can not be null at the same time")
  352. }
  353. // 设置签名类型,官方文档此接口只支持 HMAC_SHA256
  354. bm.Set("sign_type", SignType_HMAC_SHA256)
  355. bs, err := w.doProdPostTLS(ctx, bm, profitSharingMerchantRatioQuery)
  356. if err != nil {
  357. return nil, err
  358. }
  359. wxRsp = new(ProfitSharingMerchantRatioQuery)
  360. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  361. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  362. }
  363. return wxRsp, nil
  364. }
  365. // 分账回退
  366. // 对订单进行退款时,如果订单已经分账,可以先调用此接口将指定的金额从分账接收方(仅限商户类型的分账接收方)回退给本商户,然后再退款。
  367. // 回退以原分账请求为依据,可以对分给分账接收方的金额进行多次回退,只要满足累计回退不超过该请求中分给接收方的金额。
  368. // 此接口采用同步处理模式,即在接收到商户请求后,会实时返回处理结果
  369. // 此功能需要接收方在商户平台-交易中心-分账-分账接收设置下,开启同意分账回退后,才能使用。
  370. // 注意:请在初始化client时,调用 client 添加证书的相关方法添加证书
  371. // 微信文档:https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=27_7&index=7
  372. func (w *Client) ProfitSharingReturn(ctx context.Context, bm gopay.BodyMap) (wxRsp *ProfitSharingReturnResponse, err error) {
  373. err = bm.CheckEmptyError("nonce_str", "out_return_no", "return_account_type", "return_account", "return_amount", "description")
  374. if err != nil {
  375. return nil, err
  376. }
  377. if (bm.GetString("order_id") == gopay.NULL) && (bm.GetString("out_order_no") == gopay.NULL) {
  378. return nil, errors.New("param order_id and out_order_no can not be null at the same time")
  379. }
  380. // 设置签名类型,官方文档此接口只支持 HMAC_SHA256
  381. bm.Set("sign_type", SignType_HMAC_SHA256)
  382. bs, err := w.doProdPostTLS(ctx, bm, profitSharingReturn)
  383. if err != nil {
  384. return nil, err
  385. }
  386. wxRsp = new(ProfitSharingReturnResponse)
  387. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  388. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  389. }
  390. return wxRsp, nil
  391. }
  392. // 回退结果查询
  393. // 商户需要核实回退结果,可调用此接口查询回退结果。
  394. // 如果分账回退接口返回状态为处理中,可调用此接口查询回退结果
  395. // 微信文档:https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=27_8&index=8
  396. func (w *Client) ProfitSharingReturnQuery(ctx context.Context, bm gopay.BodyMap) (wxRsp *ProfitSharingReturnResponse, err error) {
  397. err = bm.CheckEmptyError("nonce_str", "out_return_no")
  398. if err != nil {
  399. return nil, err
  400. }
  401. if (bm.GetString("order_id") == gopay.NULL) && (bm.GetString("out_order_no") == gopay.NULL) {
  402. return nil, errors.New("param order_id and out_order_no can not be null at the same time")
  403. }
  404. // 设置签名类型,官方文档此接口只支持 HMAC_SHA256
  405. bm.Set("sign_type", SignType_HMAC_SHA256)
  406. bs, err := w.doProdPost(ctx, bm, profitSharingReturnQuery)
  407. if err != nil {
  408. return nil, err
  409. }
  410. wxRsp = new(ProfitSharingReturnResponse)
  411. if err = xml.Unmarshal(bs, wxRsp); err != nil {
  412. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  413. }
  414. return wxRsp, nil
  415. }