Files
kra-oa/internal/data/payment/douyin.go
T
2026-08-19 18:06:03 +08:00

231 lines
8.2 KiB
Go

package payment
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"kra/internal/biz"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/douyin"
)
type douyinAdapter struct{}
func (a *douyinAdapter) client(c map[string]any) (*douyin.Client, error) {
client, err := douyin.NewClient(text(c, "merchant_id"), text(c, "serial_no"), text(c, "api_key"), text(c, "private_key"))
if err != nil {
return nil, err
}
platformSerial := firstAny(c, "platform_serial_no", "platform_cert_serial")
if cert := text(c, "platform_cert"); cert != "" {
if platformSerial == "" {
return nil, errors.New("抖音支付缺少 platform_serial_no")
}
if err := client.SetPlatformCert([]byte(cert), platformSerial); err != nil {
return nil, err
}
}
if proxy := text(c, "proxy_url"); proxy != "" {
client.SetProxyHost(proxy)
}
return client, nil
}
func (a *douyinAdapter) 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("appid", text(c, "app_id"))
bm.Set("description", req.Subject)
bm.Set("out_trade_no", req.TradeNo)
bm.Set("notify_url", req.NotifyURL)
bm.SetBodyMap("amount", func(amount gopay.BodyMap) {
amount.Set("total", req.Amount).Set("currency", strings.ToUpper(req.Currency))
})
mergeGoPayExtras(bm, req.Extra, "appid", "description", "out_trade_no", "notify_url", "amount", "mchid", "method", "pay_method", "trade_type", "channel")
tradeType := strings.ToLower(firstAny(req.Extra, "trade_type", "pay_type", "channel"))
if tradeType == "" {
tradeType = strings.ToLower(text(c, "trade_type"))
}
switch tradeType {
case "app", "app_pay", "apporder":
rsp, callErr := client.AppOrder(ctx, bm)
if callErr != nil {
return nil, callErr
}
if rsp == nil || rsp.Response == nil || rsp.Code != douyin.Success {
return nil, douyinError(rsp)
}
payload, _ := json.Marshal(rsp.Response)
return &biz.PaymentResult{Provider: biz.PaymentDouyin, Status: "client_pending", TradeNo: req.TradeNo, QueryID: rsp.Response.PrepayId, Payload: payload}, nil
case "h5", "h5_pay":
rsp, callErr := client.H5Order(ctx, bm)
if callErr != nil {
return nil, callErr
}
if rsp == nil || rsp.Response == nil || rsp.Code != douyin.Success {
return nil, douyinError(rsp)
}
payload, _ := json.Marshal(rsp.Response)
return &biz.PaymentResult{Provider: biz.PaymentDouyin, Status: "client_pending", TradeNo: req.TradeNo, QueryID: rsp.Response.H5Url, Payload: payload}, nil
case "native", "qr", "qrcode":
rsp, callErr := client.NativeOrder(ctx, bm)
if callErr != nil {
return nil, callErr
}
if rsp == nil || rsp.Response == nil || rsp.Code != douyin.Success {
return nil, douyinError(rsp)
}
payload, _ := json.Marshal(rsp.Response)
return &biz.PaymentResult{Provider: biz.PaymentDouyin, Status: "client_pending", TradeNo: req.TradeNo, QueryID: rsp.Response.CodeUrl, Payload: payload}, nil
default:
rsp, callErr := client.JsapiOrder(ctx, bm)
if callErr != nil {
return nil, callErr
}
if rsp == nil || rsp.Response == nil || rsp.Code != douyin.Success {
return nil, douyinError(rsp)
}
payload, _ := json.Marshal(rsp.Response)
return &biz.PaymentResult{Provider: biz.PaymentDouyin, Status: "client_pending", TradeNo: req.TradeNo, QueryID: rsp.Response.PrepayId, Payload: payload}, nil
}
}
func (a *douyinAdapter) Query(ctx context.Context, tradeNo string, c map[string]any) (*biz.PaymentResult, error) {
client, err := a.client(c)
if err != nil {
return nil, err
}
rsp, err := client.OrderQueryByOutTradeNo(ctx, tradeNo)
if err != nil {
return nil, err
}
if rsp == nil || rsp.Response == nil || rsp.Code != douyin.Success {
return nil, douyinError(rsp)
}
order := rsp.Response
result := &biz.PaymentResult{Provider: biz.PaymentDouyin, Status: normalizeDouyinState(order.TradeState), TradeNo: order.OutTradeNo, ProviderTradeNo: order.TransactionId, Payload: mustJSON(order)}
if result.TradeNo == "" {
result.TradeNo = tradeNo
}
if order.Amount != nil {
result.Amount = int64(order.Amount.Total)
result.Currency = strings.ToUpper(order.Amount.Currency)
result.PayerPaidAmount = int64(order.Amount.PayerTotal)
result.PayerCurrency = strings.ToUpper(order.Amount.PayerCurrency)
if result.PayerCurrency == "" {
result.PayerCurrency = result.Currency
}
if result.PayerPaidAmount > 0 && result.Amount >= result.PayerPaidAmount {
result.CashPaidAmount = result.PayerPaidAmount
result.DiscountAmount = result.Amount - result.PayerPaidAmount
result.AmountBreakdownKnown = true
}
}
return result, nil
}
func (a *douyinAdapter) Refund(ctx context.Context, req *biz.PaymentRefundRequest, c map[string]any) (*biz.PaymentResult, error) {
if req == nil {
return nil, errors.New("抖音支付退款请求为空")
}
client, err := a.client(c)
if err != nil {
return nil, err
}
bm := gopay.BodyMap{}
bm.Set("out_trade_no", req.TradeNo).Set("out_refund_no", req.RefundNo)
bm.SetBodyMap("amount", func(m gopay.BodyMap) {
m.Set("refund", req.Amount).Set("total", req.TotalAmount).Set("currency", strings.ToUpper(req.Currency))
})
mergeGoPayConfigExtras(bm, c, "refund_extra", "out_trade_no", "out_refund_no", "amount", "mchid")
rsp, err := client.Refund(ctx, bm)
if err != nil {
return nil, err
}
if rsp == nil || rsp.Response == nil || rsp.Code != douyin.Success {
return nil, douyinError(rsp)
}
r := rsp.Response
result := &biz.PaymentResult{Provider: biz.PaymentDouyin, Status: normalizeRefundState(r.Status), TradeNo: r.OutTradeNo, ProviderTradeNo: r.RefundId, Payload: mustJSON(r)}
if result.TradeNo == "" {
result.TradeNo = req.TradeNo
}
if r.Amount != nil {
result.Amount = int64(r.Amount.Refund)
result.Currency = strings.ToUpper(r.Amount.Currency)
}
return result, nil
}
func (a *douyinAdapter) Callback(ctx context.Context, callback *biz.PaymentCallback, c map[string]any) (*biz.PaymentResult, error) {
if callback == nil {
return nil, errors.New("抖音支付回调为空")
}
request, err := callbackRequest(callback)
if err != nil {
return nil, err
}
notify, err := douyin.ParseNotify(request)
if err != nil {
return nil, err
}
client, err := a.client(c)
if err != nil {
return nil, err
}
if cert := text(c, "platform_cert"); cert == "" {
return nil, errors.New("抖音支付回调缺少 platform_cert")
}
if err := notify.VerifySignByPKMap(client.PlatformCertMap()); err != nil {
return nil, err
}
if strings.EqualFold(notify.EventType, douyin.EventTransactionSuccess) {
pay, err := notify.DecryptPayCipherText(text(c, "api_key"))
if err != nil {
return nil, err
}
result := &biz.PaymentResult{Provider: biz.PaymentDouyin, Status: normalizeDouyinState(pay.TradeState), TradeNo: pay.OutTradeNo, ProviderTradeNo: pay.TransactionId, Payload: mustJSON(pay), EventID: notify.Id}
if pay.Amount != nil {
result.Amount = int64(pay.Amount.Total)
result.Currency = strings.ToUpper(pay.Amount.Currency)
result.PayerPaidAmount = int64(pay.Amount.PayerTotal)
result.PayerCurrency = strings.ToUpper(pay.Amount.PayerCurrency)
if result.PayerCurrency == "" {
result.PayerCurrency = result.Currency
}
if result.PayerPaidAmount > 0 && result.Amount >= result.PayerPaidAmount {
result.CashPaidAmount = result.PayerPaidAmount
result.DiscountAmount = result.Amount - result.PayerPaidAmount
result.AmountBreakdownKnown = true
}
}
return result, nil
}
if strings.HasPrefix(strings.ToUpper(notify.EventType), "REFUND.") {
refund, err := notify.DecryptRefundCipherText(text(c, "api_key"))
if err != nil {
return nil, err
}
status := "pending"
if strings.EqualFold(refund.RefundStatus, douyin.RefundStatusSuccess) {
status = "success"
} else if strings.EqualFold(refund.RefundStatus, douyin.RefundStatusClosed) || strings.EqualFold(refund.RefundStatus, douyin.RefundStatusAbnormal) {
status = "failed"
}
result := &biz.PaymentResult{Provider: biz.PaymentDouyin, Status: status, TradeNo: refund.OutTradeNo, ProviderTradeNo: refund.RefundId, Payload: mustJSON(refund), EventID: notify.Id}
if refund.Amount != nil {
result.Amount = int64(refund.Amount.Refund)
result.Currency = strings.ToUpper(firstAny(c, "currency"))
}
return result, nil
}
return nil, fmt.Errorf("抖音支付不支持回调事件 %q", notify.EventType)
}