161 lines
6.0 KiB
Go
161 lines
6.0 KiB
Go
package payment
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"kra/internal/biz"
|
||
|
||
"github.com/go-pay/gopay"
|
||
gopayQQ "github.com/go-pay/gopay/qq"
|
||
)
|
||
|
||
type qqAdapter struct{}
|
||
|
||
func (a *qqAdapter) client(c map[string]any) (*gopayQQ.Client, error) {
|
||
mch := firstAny(c, "merchant_id", "mch_id")
|
||
if mch == "" || text(c, "api_key") == "" {
|
||
return nil, errors.New("QQ 支付缺少 merchant_id 或 api_key")
|
||
}
|
||
client := gopayQQ.NewClient(mch, text(c, "api_key"))
|
||
return client, nil
|
||
}
|
||
|
||
func (a *qqAdapter) Create(ctx context.Context, req *biz.PaymentRequest, c map[string]any) (*biz.PaymentResult, error) {
|
||
client, err := a.client(c)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
bm := gopay.BodyMap{}
|
||
bm.Set("nonce_str", nonce()).Set("body", req.Subject).Set("out_trade_no", req.TradeNo).Set("total_fee", req.Amount).Set("spbill_create_ip", req.ClientIP).Set("notify_url", req.NotifyURL)
|
||
tradeType := firstAny(req.Extra, "trade_type", "pay_type")
|
||
if tradeType == "" {
|
||
tradeType = firstAny(c, "trade_type", "pay_type")
|
||
}
|
||
if tradeType == "" {
|
||
tradeType = gopayQQ.TradeType_Native
|
||
}
|
||
bm.Set("trade_type", tradeType)
|
||
mergeGoPayExtras(bm, req.Extra, "nonce_str", "body", "out_trade_no", "total_fee", "spbill_create_ip", "notify_url", "trade_type", "mch_id")
|
||
rsp, err := client.UnifiedOrder(ctx, bm)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if rsp == nil || !qqOK(rsp.ReturnCode, rsp.ResultCode) {
|
||
return nil, fmt.Errorf("QQ 支付下单失败: %s", first(rspMap(rsp), "return_msg", "ret_msg", "err_code_des"))
|
||
}
|
||
result := &biz.PaymentResult{Provider: biz.PaymentQQ, Status: "created", TradeNo: req.TradeNo, QueryID: first(rspMap(rsp), "prepay_id", "code_url"), Payload: mustJSON(rsp)}
|
||
return result, nil
|
||
}
|
||
|
||
func (a *qqAdapter) Query(ctx context.Context, tradeNo string, c map[string]any) (*biz.PaymentResult, error) {
|
||
client, err := a.client(c)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
bm := gopay.BodyMap{"nonce_str": nonce(), "out_trade_no": tradeNo}
|
||
mergeGoPayConfigExtras(bm, c, "query_extra", "nonce_str", "out_trade_no", "mch_id")
|
||
rsp, err := client.OrderQuery(ctx, bm)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if rsp == nil || !qqOK(rsp.ReturnCode, rsp.ResultCode) {
|
||
return nil, fmt.Errorf("QQ 支付查单失败: %s", first(rspMap(rsp), "return_msg", "ret_msg", "err_code_des"))
|
||
}
|
||
result := &biz.PaymentResult{Provider: biz.PaymentQQ, Status: normalizeQQState(rsp.TradeState), TradeNo: rsp.OutTradeNo, ProviderTradeNo: rsp.TransactionId, Payload: mustJSON(rsp)}
|
||
if result.TradeNo == "" {
|
||
result.TradeNo = tradeNo
|
||
}
|
||
result.Currency = strings.ToUpper(rsp.FeeType)
|
||
if result.Currency == "" {
|
||
result.Currency = "CNY"
|
||
}
|
||
if rsp.TotalFee != "" {
|
||
result.Amount, err = parseIntegerAmount(rsp.TotalFee)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
if rsp.CashFee != "" {
|
||
result.PayerPaidAmount, _ = parseIntegerAmount(rsp.CashFee)
|
||
result.CashPaidAmount = result.PayerPaidAmount
|
||
result.AmountBreakdownKnown = true
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (a *qqAdapter) Refund(ctx context.Context, req *biz.PaymentRefundRequest, c map[string]any) (*biz.PaymentResult, error) {
|
||
if req == nil {
|
||
return nil, errors.New("QQ 支付退款请求为空")
|
||
}
|
||
client, err := a.client(c)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
bm := gopay.BodyMap{"nonce_str": nonce(), "out_trade_no": req.TradeNo, "out_refund_no": req.RefundNo, "refund_fee": req.Amount, "op_user_id": firstAny(c, "op_user_id", "merchant_id", "mch_id"), "op_user_passwd": text(c, "op_user_passwd")}
|
||
if req.TotalAmount > 0 {
|
||
bm.Set("total_fee", req.TotalAmount)
|
||
}
|
||
if req.Currency != "" {
|
||
bm.Set("refund_fee_type", strings.ToUpper(req.Currency))
|
||
}
|
||
mergeGoPayConfigExtras(bm, c, "refund_extra", "nonce_str", "out_trade_no", "out_refund_no", "refund_fee", "total_fee", "op_user_id", "op_user_passwd", "mch_id")
|
||
cert, key, p12 := anyConfig(c, "cert_file", "cert_path"), anyConfig(c, "key_file", "key_path"), anyConfig(c, "pkcs12_file", "p12_file")
|
||
if (cert == nil || key == nil) && p12 == nil {
|
||
if text(c, "cert_content") != "" || text(c, "key_content") != "" || text(c, "pkcs12_content") != "" {
|
||
cert, key, p12 = []byte(text(c, "cert_content")), []byte(text(c, "key_content")), []byte(text(c, "pkcs12_content"))
|
||
}
|
||
}
|
||
if (cert == nil || key == nil) && p12 == nil {
|
||
return nil, errors.New("QQ 支付退款缺少 cert_file、key_file、pkcs12_file(或 *_content)")
|
||
}
|
||
rsp, err := client.Refund(ctx, bm, cert, key, p12)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if rsp == nil || !qqOK(rsp.ReturnCode, rsp.ResultCode) {
|
||
return nil, fmt.Errorf("QQ 支付退款失败: %s", first(rspMap(rsp), "return_msg", "ret_msg", "err_code_des"))
|
||
}
|
||
refundAmount, _ := parseIntegerAmount(rsp.RefundFee)
|
||
returnedTradeNo := rsp.OutTradeNo
|
||
if returnedTradeNo == "" {
|
||
returnedTradeNo = req.TradeNo
|
||
}
|
||
return &biz.PaymentResult{Provider: biz.PaymentQQ, Status: "pending", TradeNo: returnedTradeNo, ProviderTradeNo: rsp.RefundId, Amount: refundAmount, Currency: strings.ToUpper(req.Currency), Payload: mustJSON(rsp)}, nil
|
||
}
|
||
|
||
func (a *qqAdapter) Callback(_ context.Context, callback *biz.PaymentCallback, c map[string]any) (*biz.PaymentResult, error) {
|
||
if callback == nil {
|
||
return nil, errors.New("QQ 支付回调为空")
|
||
}
|
||
request, err := callbackRequest(callback)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
bm, err := gopayQQ.ParseNotifyToBodyMap(request)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
signType := firstAny(c, "sign_type", "signature_type", "signType")
|
||
if signType == "" {
|
||
signType = gopayQQ.SignType_MD5
|
||
}
|
||
ok, err := gopayQQ.VerifySign(text(c, "api_key"), signType, cloneGoPayBodyMap(bm))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !ok {
|
||
return nil, errors.New("QQ 支付回调签名校验失败")
|
||
}
|
||
values := fromGoPayBodyMap(bm)
|
||
status := "pending"
|
||
if strings.EqualFold(values["return_code"], "SUCCESS") && strings.EqualFold(values["result_code"], "SUCCESS") && strings.EqualFold(values["trade_state"], "SUCCESS") {
|
||
status = "success"
|
||
}
|
||
payload, _ := json.Marshal(values)
|
||
return &biz.PaymentResult{Provider: biz.PaymentQQ, Status: status, TradeNo: values["out_trade_no"], ProviderTradeNo: values["transaction_id"], Payload: payload}, nil
|
||
}
|