package qq import ( "context" "crypto/hmac" "crypto/md5" "crypto/sha256" "crypto/tls" "encoding/xml" "errors" "fmt" "hash" "strings" "sync" "github.com/go-pay/gopay" "github.com/go-pay/gopay/pkg/xhttp" "github.com/go-pay/xlog" ) type Client struct { MchId string ApiKey string IsProd bool DebugSwitch gopay.DebugSwitch logger xlog.XLogger mu sync.RWMutex sha256Hash hash.Hash md5Hash hash.Hash hc *xhttp.Client tlsHc *xhttp.Client } // 初始化QQ客户端(正式环境) // mchId:商户ID // ApiKey:API秘钥值 func NewClient(mchId, apiKey string) (client *Client) { logger := xlog.NewLogger() logger.SetLevel(xlog.DebugLevel) return &Client{ MchId: mchId, ApiKey: apiKey, DebugSwitch: gopay.DebugOff, logger: logger, sha256Hash: hmac.New(sha256.New, []byte(apiKey)), md5Hash: md5.New(), hc: xhttp.NewClient(), tlsHc: xhttp.NewClient(), } } // SetBodySize 设置http response body size(MB) func (q *Client) SetBodySize(sizeMB int) { if sizeMB > 0 { q.hc.SetBodySize(sizeMB) } } // SetHttpClient 设置自定义的xhttp.Client func (q *Client) SetHttpClient(client *xhttp.Client) { if client != nil { q.hc = client } } // SetTLSHttpClient 设置自定义的xhttp.Client func (q *Client) SetTLSHttpClient(client *xhttp.Client) { if client != nil { q.tlsHc = client } } func (q *Client) SetLogger(logger xlog.XLogger) { if logger != nil { q.logger = logger } } // 向QQ发送Post请求,对于本库未提供的QQ API,可自行实现,通过此方法发送请求 // bm:请求参数的BodyMap // url:完整url地址,例如:https://qpay.qq.com/cgi-bin/pay/qpay_unified_order.cgi // tlsConfig:tls配置,如无需证书请求,传nil func (q *Client) PostQQAPISelf(ctx context.Context, bm gopay.BodyMap, url string, tlsConfig *tls.Config) (bs []byte, err error) { if bm.GetString("mch_id") == gopay.NULL { bm.Set("mch_id", q.MchId) } if bm.GetString("fee_type") == gopay.NULL { bm.Set("fee_type", "CNY") } if bm.GetString("sign") == gopay.NULL { sign := GetReleaseSign(q.ApiKey, bm.GetString("sign_type"), bm) bm.Set("sign", sign) } req := GenerateXml(bm) if q.DebugSwitch == gopay.DebugOn { q.logger.Debugf("QQ_Request: %s", req) } httpClient := xhttp.NewClient() if q.IsProd && tlsConfig != nil { httpClient.SetTLSConfig(tlsConfig) } res, bs, err := httpClient.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx) if err != nil { return nil, err } if q.DebugSwitch == gopay.DebugOn { q.logger.Debugf("QQ_Response: %d, %s", res.StatusCode, string(bs)) } if res.StatusCode != 200 { return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode) } if strings.Contains(string(bs), "HTML") { return nil, errors.New(string(bs)) } return bs, nil } // 提交付款码支付 // 文档地址:https://qpay.qq.com/buss/wiki/1/1122 func (q *Client) MicroPay(ctx context.Context, bm gopay.BodyMap) (qqRsp *MicroPayResponse, err error) { err = bm.CheckEmptyError("nonce_str", "body", "out_trade_no", "total_fee", "spbill_create_ip", "device_info", "auth_code") if err != nil { return nil, err } bm.Set("trade_type", TradeType_MicroPay) bs, err := q.doQQPost(ctx, bm, microPay) if err != nil { return nil, err } qqRsp = new(MicroPayResponse) if err = xml.Unmarshal(bs, qqRsp); err != nil { return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs)) } return qqRsp, nil } // 撤销订单 // 文档地址:https://qpay.qq.com/buss/wiki/1/1125 func (q *Client) Reverse(ctx context.Context, bm gopay.BodyMap) (qqRsp *ReverseResponse, err error) { err = bm.CheckEmptyError("sub_mch_id", "nonce_str", "out_trade_no", "op_user_id", "op_user_passwd") if err != nil { return nil, err } bs, err := q.doQQPost(ctx, bm, reverse) if err != nil { return nil, err } qqRsp = new(ReverseResponse) if err = xml.Unmarshal(bs, qqRsp); err != nil { return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs)) } return qqRsp, nil } // 统一下单 // 文档地址:https://qpay.qq.com/buss/wiki/38/1203 func (q *Client) UnifiedOrder(ctx context.Context, bm gopay.BodyMap) (qqRsp *UnifiedOrderResponse, err error) { err = bm.CheckEmptyError("nonce_str", "body", "out_trade_no", "total_fee", "spbill_create_ip", "trade_type", "notify_url") if err != nil { return nil, err } bs, err := q.doQQPost(ctx, bm, unifiedOrder) if err != nil { return nil, err } qqRsp = new(UnifiedOrderResponse) if err = xml.Unmarshal(bs, qqRsp); err != nil { return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs)) } return qqRsp, nil } // 订单查询 // 文档地址:https://qpay.qq.com/buss/wiki/38/1205 func (q *Client) OrderQuery(ctx context.Context, bm gopay.BodyMap) (qqRsp *OrderQueryResponse, err error) { err = bm.CheckEmptyError("nonce_str") if err != nil { return nil, err } if bm.GetString("out_trade_no") == gopay.NULL && bm.GetString("transaction_id") == gopay.NULL { return nil, errors.New("out_trade_no and transaction_id are not allowed to be null at the same time") } bs, err := q.doQQPost(ctx, bm, orderQuery) if err != nil { return nil, err } qqRsp = new(OrderQueryResponse) if err = xml.Unmarshal(bs, qqRsp); err != nil { return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs)) } return qqRsp, nil } // 关闭订单 // 文档地址:https://qpay.qq.com/buss/wiki/38/1206 func (q *Client) CloseOrder(ctx context.Context, bm gopay.BodyMap) (qqRsp *CloseOrderResponse, err error) { err = bm.CheckEmptyError("nonce_str", "out_trade_no") if err != nil { return nil, err } bs, err := q.doQQPost(ctx, bm, orderClose) if err != nil { return nil, err } qqRsp = new(CloseOrderResponse) if err = xml.Unmarshal(bs, qqRsp); err != nil { return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs)) } return qqRsp, nil } // 申请退款 // 注意:如已使用client.AddCertFilePath()添加过证书,参数certFilePath、keyFilePath、pkcs12FilePath全传空字符串 nil,否则,3证书Path均不可空 // 文档地址:https://qpay.qq.com/buss/wiki/38/1207 func (q *Client) Refund(ctx context.Context, bm gopay.BodyMap, certFilePath, keyFilePath, pkcs12FilePath any) (qqRsp *RefundResponse, err error) { if err = checkCertFilePathOrContent(certFilePath, keyFilePath, pkcs12FilePath); err != nil { return nil, err } err = bm.CheckEmptyError("nonce_str", "out_refund_no", "refund_fee", "op_user_id", "op_user_passwd") if err != nil { return nil, err } if bm.GetString("out_trade_no") == gopay.NULL && bm.GetString("transaction_id") == gopay.NULL { return nil, errors.New("out_trade_no and transaction_id are not allowed to be null at the same time") } bs, err := q.doQQPostTLS(ctx, bm, refund) if err != nil { return nil, err } qqRsp = new(RefundResponse) if err = xml.Unmarshal(bs, qqRsp); err != nil { return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs)) } return qqRsp, nil } // 退款查询 // 文档地址:https://qpay.qq.com/buss/wiki/38/1208 func (q *Client) RefundQuery(ctx context.Context, bm gopay.BodyMap) (qqRsp *RefundQueryResponse, err error) { err = bm.CheckEmptyError("nonce_str") if err != nil { return nil, err } 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 { return nil, errors.New("refund_id, out_refund_no, out_trade_no, transaction_id are not allowed to be null at the same time") } bs, err := q.doQQPost(ctx, bm, refundQuery) if err != nil { return nil, err } qqRsp = new(RefundQueryResponse) if err = xml.Unmarshal(bs, qqRsp); err != nil { return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs)) } return qqRsp, nil } // 交易账单 // 文档地址:https://qpay.qq.com/buss/wiki/38/1209 func (q *Client) StatementDown(ctx context.Context, bm gopay.BodyMap) (qqRsp string, err error) { err = bm.CheckEmptyError("nonce_str", "bill_date", "bill_type") if err != nil { return gopay.NULL, err } billType := bm.GetString("bill_type") if billType != "ALL" && billType != "SUCCESS" && billType != "REFUND" && billType != "RECHAR" { return gopay.NULL, errors.New("bill_type error, please reference: https://qpay.qq.com/buss/wiki/38/1209") } bs, err := q.doQQPost(ctx, bm, statementDown) if err != nil { return gopay.NULL, err } return string(bs), nil } // 资金账单 // 文档地址:https://qpay.qq.com/buss/wiki/38/3089 func (q *Client) AccRoll(ctx context.Context, bm gopay.BodyMap) (qqRsp string, err error) { err = bm.CheckEmptyError("nonce_str", "bill_date", "acc_type") if err != nil { return gopay.NULL, err } accType := bm.GetString("acc_type") if accType != "CASH" && accType != "MARKETING" { return gopay.NULL, errors.New("acc_type error, please reference: https://qpay.qq.com/buss/wiki/38/3089") } bs, err := q.doQQPost(ctx, bm, accRoll) if err != nil { return gopay.NULL, err } return string(bs), nil } // 向QQ发送请求 func (q *Client) doQQPost(ctx context.Context, bm gopay.BodyMap, url string) (bs []byte, err error) { if bm.GetString("mch_id") == gopay.NULL { bm.Set("mch_id", q.MchId) } if bm.GetString("fee_type") == gopay.NULL { bm.Set("fee_type", "CNY") } if bm.GetString("sign") == gopay.NULL { sign := q.getReleaseSign(q.ApiKey, bm.GetString("sign_type"), bm) bm.Set("sign", sign) } req := GenerateXml(bm) if q.DebugSwitch == gopay.DebugOn { q.logger.Debugf("QQ_Request: %s", req) } res, bs, err := q.hc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx) if err != nil { return nil, err } if q.DebugSwitch == gopay.DebugOn { q.logger.Debugf("QQ_Response: %d, %s", res.StatusCode, string(bs)) } if res.StatusCode != 200 { return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode) } if strings.Contains(string(bs), "HTML") { return nil, errors.New(string(bs)) } return bs, nil } // 向QQ发送请求 TLS func (q *Client) doQQPostTLS(ctx context.Context, bm gopay.BodyMap, url string) (bs []byte, err error) { if bm.GetString("mch_id") == gopay.NULL { bm.Set("mch_id", q.MchId) } if bm.GetString("fee_type") == gopay.NULL { bm.Set("fee_type", "CNY") } if bm.GetString("sign") == gopay.NULL { sign := q.getReleaseSign(q.ApiKey, bm.GetString("sign_type"), bm) bm.Set("sign", sign) } req := GenerateXml(bm) if q.DebugSwitch == gopay.DebugOn { q.logger.Debugf("QQ_Request: %s", req) } res, bs, err := q.tlsHc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx) if err != nil { return nil, err } if q.DebugSwitch == gopay.DebugOn { q.logger.Debugf("QQ_Response: %d, %s", res.StatusCode, string(bs)) } if res.StatusCode != 200 { return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode) } if strings.Contains(string(bs), "HTML") { return nil, errors.New(string(bs)) } return bs, nil } // Get请求、正式 func (q *Client) doQQGet(ctx context.Context, bm gopay.BodyMap, url, signType string) (bs []byte, err error) { if bm.GetString("mch_id") == gopay.NULL { bm.Set("mch_id", q.MchId) } bm.Remove("sign") sign := q.getReleaseSign(q.ApiKey, signType, bm) bm.Set("sign", sign) if q.DebugSwitch == gopay.DebugOn { q.logger.Debugf("QQ_Request: %s", bm.JsonBody()) } param := bm.EncodeURLParams() uri := url + "?" + param res, bs, err := q.hc.Req(xhttp.TypeXML).Get(uri).EndBytes(ctx) if err != nil { return nil, err } if q.DebugSwitch == gopay.DebugOn { q.logger.Debugf("QQ_Response: %d, %s", res.StatusCode, string(bs)) } if res.StatusCode != 200 { return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode) } if strings.Contains(string(bs), "HTML") || strings.Contains(string(bs), "html") { return nil, errors.New(string(bs)) } return bs, nil } func (q *Client) doQQRed(ctx context.Context, bm gopay.BodyMap, url string) (bs []byte, err error) { if bm.GetString("mch_id") == gopay.NULL { bm.Set("mch_id", q.MchId) } if bm.GetString("sign") == gopay.NULL { sign := GetReleaseSign(q.ApiKey, SignType_MD5, bm) bm.Set("sign", sign) } req := GenerateXml(bm) if q.DebugSwitch == gopay.DebugOn { q.logger.Debugf("QQ_Request: %s", req) } res, bs, err := q.tlsHc.Req(xhttp.TypeXML).Post(url).SendString(req).EndBytes(ctx) if err != nil { return nil, err } if q.DebugSwitch == gopay.DebugOn { q.logger.Debugf("QQ_Response: %d, %s", res.StatusCode, string(bs)) } if res.StatusCode != 200 { return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode) } if strings.Contains(string(bs), "HTML") { return nil, errors.New(string(bs)) } return bs, nil }