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

414 line
13 KiB

  1. package qq
  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. MchId string
  20. ApiKey string
  21. IsProd bool
  22. DebugSwitch gopay.DebugSwitch
  23. logger xlog.XLogger
  24. mu sync.RWMutex
  25. sha256Hash hash.Hash
  26. md5Hash hash.Hash
  27. hc *xhttp.Client
  28. tlsHc *xhttp.Client
  29. }
  30. // 初始化QQ客户端(正式环境)
  31. // mchId:商户ID
  32. // ApiKey:API秘钥值
  33. func NewClient(mchId, apiKey string) (client *Client) {
  34. logger := xlog.NewLogger()
  35. logger.SetLevel(xlog.DebugLevel)
  36. return &Client{
  37. MchId: mchId,
  38. ApiKey: apiKey,
  39. DebugSwitch: gopay.DebugOff,
  40. logger: logger,
  41. sha256Hash: hmac.New(sha256.New, []byte(apiKey)),
  42. md5Hash: md5.New(),
  43. hc: xhttp.NewClient(),
  44. tlsHc: xhttp.NewClient(),
  45. }
  46. }
  47. // SetBodySize 设置http response body size(MB)
  48. func (q *Client) SetBodySize(sizeMB int) {
  49. if sizeMB > 0 {
  50. q.hc.SetBodySize(sizeMB)
  51. }
  52. }
  53. // SetHttpClient 设置自定义的xhttp.Client
  54. func (q *Client) SetHttpClient(client *xhttp.Client) {
  55. if client != nil {
  56. q.hc = client
  57. }
  58. }
  59. // SetTLSHttpClient 设置自定义的xhttp.Client
  60. func (q *Client) SetTLSHttpClient(client *xhttp.Client) {
  61. if client != nil {
  62. q.tlsHc = client
  63. }
  64. }
  65. func (q *Client) SetLogger(logger xlog.XLogger) {
  66. if logger != nil {
  67. q.logger = logger
  68. }
  69. }
  70. // 向QQ发送Post请求,对于本库未提供的QQ API,可自行实现,通过此方法发送请求
  71. // bm:请求参数的BodyMap
  72. // url:完整url地址,例如:https://qpay.qq.com/cgi-bin/pay/qpay_unified_order.cgi
  73. // tlsConfig:tls配置,如无需证书请求,传nil
  74. func (q *Client) PostQQAPISelf(ctx context.Context, bm gopay.BodyMap, url string, tlsConfig *tls.Config) (bs []byte, err error) {
  75. if bm.GetString("mch_id") == gopay.NULL {
  76. bm.Set("mch_id", q.MchId)
  77. }
  78. if bm.GetString("fee_type") == gopay.NULL {
  79. bm.Set("fee_type", "CNY")
  80. }
  81. if bm.GetString("sign") == gopay.NULL {
  82. sign := GetReleaseSign(q.ApiKey, bm.GetString("sign_type"), bm)
  83. bm.Set("sign", sign)
  84. }
  85. req := GenerateXml(bm)
  86. if q.DebugSwitch == gopay.DebugOn {
  87. q.logger.Debugf("QQ_Request: %s", req)
  88. }
  89. httpClient := xhttp.NewClient()
  90. if q.IsProd && tlsConfig != nil {
  91. httpClient.SetTLSConfig(tlsConfig)
  92. }
  93. res, bs, err := httpClient.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  94. if err != nil {
  95. return nil, err
  96. }
  97. if q.DebugSwitch == gopay.DebugOn {
  98. q.logger.Debugf("QQ_Response: %d, %s", res.StatusCode, string(bs))
  99. }
  100. if res.StatusCode != 200 {
  101. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  102. }
  103. if strings.Contains(string(bs), "HTML") {
  104. return nil, errors.New(string(bs))
  105. }
  106. return bs, nil
  107. }
  108. // 提交付款码支付
  109. // 文档地址:https://qpay.qq.com/buss/wiki/1/1122
  110. func (q *Client) MicroPay(ctx context.Context, bm gopay.BodyMap) (qqRsp *MicroPayResponse, err error) {
  111. err = bm.CheckEmptyError("nonce_str", "body", "out_trade_no", "total_fee", "spbill_create_ip", "device_info", "auth_code")
  112. if err != nil {
  113. return nil, err
  114. }
  115. bm.Set("trade_type", TradeType_MicroPay)
  116. bs, err := q.doQQPost(ctx, bm, microPay)
  117. if err != nil {
  118. return nil, err
  119. }
  120. qqRsp = new(MicroPayResponse)
  121. if err = xml.Unmarshal(bs, qqRsp); err != nil {
  122. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  123. }
  124. return qqRsp, nil
  125. }
  126. // 撤销订单
  127. // 文档地址:https://qpay.qq.com/buss/wiki/1/1125
  128. func (q *Client) Reverse(ctx context.Context, bm gopay.BodyMap) (qqRsp *ReverseResponse, err error) {
  129. err = bm.CheckEmptyError("sub_mch_id", "nonce_str", "out_trade_no", "op_user_id", "op_user_passwd")
  130. if err != nil {
  131. return nil, err
  132. }
  133. bs, err := q.doQQPost(ctx, bm, reverse)
  134. if err != nil {
  135. return nil, err
  136. }
  137. qqRsp = new(ReverseResponse)
  138. if err = xml.Unmarshal(bs, qqRsp); err != nil {
  139. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  140. }
  141. return qqRsp, nil
  142. }
  143. // 统一下单
  144. // 文档地址:https://qpay.qq.com/buss/wiki/38/1203
  145. func (q *Client) UnifiedOrder(ctx context.Context, bm gopay.BodyMap) (qqRsp *UnifiedOrderResponse, err error) {
  146. err = bm.CheckEmptyError("nonce_str", "body", "out_trade_no", "total_fee", "spbill_create_ip", "trade_type", "notify_url")
  147. if err != nil {
  148. return nil, err
  149. }
  150. bs, err := q.doQQPost(ctx, bm, unifiedOrder)
  151. if err != nil {
  152. return nil, err
  153. }
  154. qqRsp = new(UnifiedOrderResponse)
  155. if err = xml.Unmarshal(bs, qqRsp); err != nil {
  156. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  157. }
  158. return qqRsp, nil
  159. }
  160. // 订单查询
  161. // 文档地址:https://qpay.qq.com/buss/wiki/38/1205
  162. func (q *Client) OrderQuery(ctx context.Context, bm gopay.BodyMap) (qqRsp *OrderQueryResponse, err error) {
  163. err = bm.CheckEmptyError("nonce_str")
  164. if err != nil {
  165. return nil, err
  166. }
  167. if bm.GetString("out_trade_no") == gopay.NULL && bm.GetString("transaction_id") == gopay.NULL {
  168. return nil, errors.New("out_trade_no and transaction_id are not allowed to be null at the same time")
  169. }
  170. bs, err := q.doQQPost(ctx, bm, orderQuery)
  171. if err != nil {
  172. return nil, err
  173. }
  174. qqRsp = new(OrderQueryResponse)
  175. if err = xml.Unmarshal(bs, qqRsp); err != nil {
  176. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  177. }
  178. return qqRsp, nil
  179. }
  180. // 关闭订单
  181. // 文档地址:https://qpay.qq.com/buss/wiki/38/1206
  182. func (q *Client) CloseOrder(ctx context.Context, bm gopay.BodyMap) (qqRsp *CloseOrderResponse, err error) {
  183. err = bm.CheckEmptyError("nonce_str", "out_trade_no")
  184. if err != nil {
  185. return nil, err
  186. }
  187. bs, err := q.doQQPost(ctx, bm, orderClose)
  188. if err != nil {
  189. return nil, err
  190. }
  191. qqRsp = new(CloseOrderResponse)
  192. if err = xml.Unmarshal(bs, qqRsp); err != nil {
  193. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  194. }
  195. return qqRsp, nil
  196. }
  197. // 申请退款
  198. // 注意:如已使用client.AddCertFilePath()添加过证书,参数certFilePath、keyFilePath、pkcs12FilePath全传空字符串 nil,否则,3证书Path均不可空
  199. // 文档地址:https://qpay.qq.com/buss/wiki/38/1207
  200. func (q *Client) Refund(ctx context.Context, bm gopay.BodyMap, certFilePath, keyFilePath, pkcs12FilePath any) (qqRsp *RefundResponse, err error) {
  201. if err = checkCertFilePathOrContent(certFilePath, keyFilePath, pkcs12FilePath); err != nil {
  202. return nil, err
  203. }
  204. err = bm.CheckEmptyError("nonce_str", "out_refund_no", "refund_fee", "op_user_id", "op_user_passwd")
  205. if err != nil {
  206. return nil, err
  207. }
  208. if bm.GetString("out_trade_no") == gopay.NULL && bm.GetString("transaction_id") == gopay.NULL {
  209. return nil, errors.New("out_trade_no and transaction_id are not allowed to be null at the same time")
  210. }
  211. bs, err := q.doQQPostTLS(ctx, bm, refund)
  212. if err != nil {
  213. return nil, err
  214. }
  215. qqRsp = new(RefundResponse)
  216. if err = xml.Unmarshal(bs, qqRsp); err != nil {
  217. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  218. }
  219. return qqRsp, nil
  220. }
  221. // 退款查询
  222. // 文档地址:https://qpay.qq.com/buss/wiki/38/1208
  223. func (q *Client) RefundQuery(ctx context.Context, bm gopay.BodyMap) (qqRsp *RefundQueryResponse, err error) {
  224. err = bm.CheckEmptyError("nonce_str")
  225. if err != nil {
  226. return nil, err
  227. }
  228. if bm.GetString("refund_id") == gopay.NULL && bm.GetString("out_refund_no") == gopay.NULL && bm.GetString("transaction_id") == gopay.NULL && bm.GetString("out_trade_no") == gopay.NULL {
  229. return nil, errors.New("refund_id, out_refund_no, out_trade_no, transaction_id are not allowed to be null at the same time")
  230. }
  231. bs, err := q.doQQPost(ctx, bm, refundQuery)
  232. if err != nil {
  233. return nil, err
  234. }
  235. qqRsp = new(RefundQueryResponse)
  236. if err = xml.Unmarshal(bs, qqRsp); err != nil {
  237. return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
  238. }
  239. return qqRsp, nil
  240. }
  241. // 交易账单
  242. // 文档地址:https://qpay.qq.com/buss/wiki/38/1209
  243. func (q *Client) StatementDown(ctx context.Context, bm gopay.BodyMap) (qqRsp string, err error) {
  244. err = bm.CheckEmptyError("nonce_str", "bill_date", "bill_type")
  245. if err != nil {
  246. return gopay.NULL, err
  247. }
  248. billType := bm.GetString("bill_type")
  249. if billType != "ALL" && billType != "SUCCESS" && billType != "REFUND" && billType != "RECHAR" {
  250. return gopay.NULL, errors.New("bill_type error, please reference: https://qpay.qq.com/buss/wiki/38/1209")
  251. }
  252. bs, err := q.doQQPost(ctx, bm, statementDown)
  253. if err != nil {
  254. return gopay.NULL, err
  255. }
  256. return string(bs), nil
  257. }
  258. // 资金账单
  259. // 文档地址:https://qpay.qq.com/buss/wiki/38/3089
  260. func (q *Client) AccRoll(ctx context.Context, bm gopay.BodyMap) (qqRsp string, err error) {
  261. err = bm.CheckEmptyError("nonce_str", "bill_date", "acc_type")
  262. if err != nil {
  263. return gopay.NULL, err
  264. }
  265. accType := bm.GetString("acc_type")
  266. if accType != "CASH" && accType != "MARKETING" {
  267. return gopay.NULL, errors.New("acc_type error, please reference: https://qpay.qq.com/buss/wiki/38/3089")
  268. }
  269. bs, err := q.doQQPost(ctx, bm, accRoll)
  270. if err != nil {
  271. return gopay.NULL, err
  272. }
  273. return string(bs), nil
  274. }
  275. // 向QQ发送请求
  276. func (q *Client) doQQPost(ctx context.Context, bm gopay.BodyMap, url string) (bs []byte, err error) {
  277. if bm.GetString("mch_id") == gopay.NULL {
  278. bm.Set("mch_id", q.MchId)
  279. }
  280. if bm.GetString("fee_type") == gopay.NULL {
  281. bm.Set("fee_type", "CNY")
  282. }
  283. if bm.GetString("sign") == gopay.NULL {
  284. sign := q.getReleaseSign(q.ApiKey, bm.GetString("sign_type"), bm)
  285. bm.Set("sign", sign)
  286. }
  287. req := GenerateXml(bm)
  288. if q.DebugSwitch == gopay.DebugOn {
  289. q.logger.Debugf("QQ_Request: %s", req)
  290. }
  291. res, bs, err := q.hc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  292. if err != nil {
  293. return nil, err
  294. }
  295. if q.DebugSwitch == gopay.DebugOn {
  296. q.logger.Debugf("QQ_Response: %d, %s", res.StatusCode, string(bs))
  297. }
  298. if res.StatusCode != 200 {
  299. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  300. }
  301. if strings.Contains(string(bs), "HTML") {
  302. return nil, errors.New(string(bs))
  303. }
  304. return bs, nil
  305. }
  306. // 向QQ发送请求 TLS
  307. func (q *Client) doQQPostTLS(ctx context.Context, bm gopay.BodyMap, url string) (bs []byte, err error) {
  308. if bm.GetString("mch_id") == gopay.NULL {
  309. bm.Set("mch_id", q.MchId)
  310. }
  311. if bm.GetString("fee_type") == gopay.NULL {
  312. bm.Set("fee_type", "CNY")
  313. }
  314. if bm.GetString("sign") == gopay.NULL {
  315. sign := q.getReleaseSign(q.ApiKey, bm.GetString("sign_type"), bm)
  316. bm.Set("sign", sign)
  317. }
  318. req := GenerateXml(bm)
  319. if q.DebugSwitch == gopay.DebugOn {
  320. q.logger.Debugf("QQ_Request: %s", req)
  321. }
  322. res, bs, err := q.tlsHc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  323. if err != nil {
  324. return nil, err
  325. }
  326. if q.DebugSwitch == gopay.DebugOn {
  327. q.logger.Debugf("QQ_Response: %d, %s", res.StatusCode, string(bs))
  328. }
  329. if res.StatusCode != 200 {
  330. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  331. }
  332. if strings.Contains(string(bs), "HTML") {
  333. return nil, errors.New(string(bs))
  334. }
  335. return bs, nil
  336. }
  337. // Get请求、正式
  338. func (q *Client) doQQGet(ctx context.Context, bm gopay.BodyMap, url, signType string) (bs []byte, err error) {
  339. if bm.GetString("mch_id") == gopay.NULL {
  340. bm.Set("mch_id", q.MchId)
  341. }
  342. bm.Remove("sign")
  343. sign := q.getReleaseSign(q.ApiKey, signType, bm)
  344. bm.Set("sign", sign)
  345. if q.DebugSwitch == gopay.DebugOn {
  346. q.logger.Debugf("QQ_Request: %s", bm.JsonBody())
  347. }
  348. param := bm.EncodeURLParams()
  349. uri := url + "?" + param
  350. res, bs, err := q.hc.Req(xhttp.TypeXML).Get(uri).EndBytes(ctx)
  351. if err != nil {
  352. return nil, err
  353. }
  354. if q.DebugSwitch == gopay.DebugOn {
  355. q.logger.Debugf("QQ_Response: %d, %s", res.StatusCode, string(bs))
  356. }
  357. if res.StatusCode != 200 {
  358. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  359. }
  360. if strings.Contains(string(bs), "HTML") || strings.Contains(string(bs), "html") {
  361. return nil, errors.New(string(bs))
  362. }
  363. return bs, nil
  364. }
  365. func (q *Client) doQQRed(ctx context.Context, bm gopay.BodyMap, url string) (bs []byte, err error) {
  366. if bm.GetString("mch_id") == gopay.NULL {
  367. bm.Set("mch_id", q.MchId)
  368. }
  369. if bm.GetString("sign") == gopay.NULL {
  370. sign := GetReleaseSign(q.ApiKey, SignType_MD5, bm)
  371. bm.Set("sign", sign)
  372. }
  373. req := GenerateXml(bm)
  374. if q.DebugSwitch == gopay.DebugOn {
  375. q.logger.Debugf("QQ_Request: %s", req)
  376. }
  377. res, bs, err := q.tlsHc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx)
  378. if err != nil {
  379. return nil, err
  380. }
  381. if q.DebugSwitch == gopay.DebugOn {
  382. q.logger.Debugf("QQ_Response: %d, %s", res.StatusCode, string(bs))
  383. }
  384. if res.StatusCode != 200 {
  385. return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode)
  386. }
  387. if strings.Contains(string(bs), "HTML") {
  388. return nil, errors.New(string(bs))
  389. }
  390. return bs, nil
  391. }