kra-new/internal/integration/payment/saobei.go

224 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package payment
import (
"context"
"errors"
"fmt"
bizpayment "kra/internal/biz/payment"
"kra/internal/paymentkit"
"strings"
"time"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/saobei"
)
type saobeiClient interface {
MiniPay(context.Context, gopay.BodyMap) (*saobei.MiniPayRsp, error)
BarcodePay(context.Context, gopay.BodyMap) (*saobei.BarcodePayRsp, error)
Query(context.Context, gopay.BodyMap) (*saobei.QueryRsp, error)
Refund(context.Context, gopay.BodyMap) (*saobei.RefundRsp, error)
}
type saobeiAdapter struct {
newClient func(map[string]any) (saobeiClient, error)
}
func saobeiBarcodeOK(returnCode, resultCode string) bool {
return saobeiResultOK(returnCode, resultCode)
}
func (a *saobeiAdapter) client(c map[string]any) (saobeiClient, error) {
if a.newClient != nil {
return a.newClient(c)
}
return saobei.NewClient(text(c, "inst_no"), text(c, "key"), text(c, "merchant_no"), text(c, "terminal_id"), text(c, "access_token"), !strings.EqualFold(text(c, "environment"), "sandbox"))
}
func (a *saobeiAdapter) Create(ctx context.Context, req *bizpayment.PaymentRequest, c map[string]any) (*bizpayment.PaymentResult, error) {
if req == nil {
return nil, errors.New("扫呗支付下单请求为空")
}
method, err := saobeiCreateMethod(req.Extra, c)
if err != nil {
return nil, err
}
client, err := a.client(c)
if err != nil {
return nil, err
}
now := time.Now().Format("20060102150405")
bm := gopay.BodyMap{"pay_type": firstAnyOr(req.Extra, "010", "pay_type", "payment_type"), "terminal_ip": req.ClientIP, "terminal_trace": req.TradeNo, "terminal_time": now, "total_fee": req.Amount}
mergeGoPayExtras(bm, req.Extra, "pay_type", "payment_type", "terminal_ip", "terminal_trace", "terminal_time", "total_fee", "method", "pay_method", "trade_type")
if method == "barcode" {
authNo := strings.TrimSpace(firstAny(req.Extra, "auth_no", "auth_code", "authcode", "barcode", "pay_code"))
if authNo == "" {
return nil, errors.New("扫呗付款码支付缺少 auth_no")
}
bm.Set("auth_no", authNo)
rsp, callErr := client.BarcodePay(ctx, bm)
if callErr != nil {
return nil, callErr
}
if rsp == nil || !saobeiBarcodeOK(rsp.ReturnCode, rsp.ResultCode) {
return nil, fmt.Errorf("扫呗付款码支付失败: %s", firstNonEmpty(rspMap(rsp)["return_msg"], rspMap(rsp)["result_code"]))
}
if strings.TrimSpace(rsp.OutTradeNo) == "" {
return nil, errors.New("扫呗支付下单响应缺少 out_trade_no")
}
status := "created"
if strings.TrimSpace(rsp.ResultCode) == saobei.ResultCodePaying {
status = "pending"
}
return &bizpayment.PaymentResult{Provider: bizpayment.PaymentSaobei, Status: status, TradeNo: req.TradeNo, QueryID: strings.TrimSpace(rsp.OutTradeNo), Payload: mustJSON(rsp)}, nil
}
rsp, err := client.MiniPay(ctx, bm)
if err != nil {
return nil, err
}
if rsp == nil || !saobeiOK(rsp.ReturnCode, rsp.ResultCode) {
return nil, fmt.Errorf("扫呗支付下单失败: %s", firstNonEmpty(rspMap(rsp)["return_msg"], rspMap(rsp)["result_code"]))
}
if strings.TrimSpace(rsp.OutTradeNo) == "" {
return nil, errors.New("扫呗支付下单响应缺少 out_trade_no")
}
return &bizpayment.PaymentResult{Provider: bizpayment.PaymentSaobei, Status: "client_pending", TradeNo: req.TradeNo, QueryID: strings.TrimSpace(rsp.OutTradeNo), Payload: mustJSON(rsp)}, nil
}
func saobeiCreateMethod(extra, config map[string]any) (string, error) {
value := firstAny(extra, "method", "pay_method", "trade_type")
if value == "" {
value = firstAny(config, "method", "pay_method", "trade_type")
}
normalized := paymentkit.NormalizePaymentMethod(value)
switch normalized {
case "", "mini", "mini_pay", "miniapp", "mini_app", "mini_program", "miniprogram", "jsapi", "js_api":
return "mini", nil
case "barcode", "barcode_pay", "micropay", "micro_pay", "scan", "scan_pay", "pay_code", "payment_code":
return "barcode", nil
default:
return "", fmt.Errorf("扫呗支付不支持的下单方式: %s", value)
}
}
func (a *saobeiAdapter) Query(ctx context.Context, tradeNo string, c map[string]any) (*bizpayment.PaymentResult, error) {
tradeNo = strings.TrimSpace(tradeNo)
if tradeNo == "" {
return nil, errors.New("扫呗支付查单缺少 out_trade_no")
}
client, err := a.client(c)
if err != nil {
return nil, err
}
// terminal_trace is the query request's own trace ID. The durable platform
// order identity is carried by out_trade_no and comes from PaymentResult.QueryID.
bm := gopay.BodyMap{"pay_type": firstAnyOr(c, "010", "pay_type", "payment_type"), "terminal_trace": nonce(), "terminal_time": time.Now().Format("20060102150405"), "out_trade_no": tradeNo}
mergeGoPayConfigExtras(bm, c, "query_extra", "pay_type", "terminal_trace", "terminal_time", "out_trade_no")
rsp, err := client.Query(ctx, bm)
if err != nil {
return nil, err
}
if rsp == nil {
return nil, errors.New("扫呗支付查单响应为空")
}
if !saobeiResultOK(rsp.ReturnCode, rsp.ResultCode) {
return nil, fmt.Errorf("扫呗支付查单失败: %s", firstNonEmpty(rspMap(rsp)["return_msg"], rspMap(rsp)["result_code"]))
}
queryID := strings.TrimSpace(tradeNo)
if responseQueryID := strings.TrimSpace(rsp.OutTradeNo); responseQueryID == "" {
return nil, errors.New("扫呗支付查单响应缺少 out_trade_no")
} else if responseQueryID != queryID {
return nil, errors.New("扫呗支付查单平台订单号不匹配")
}
tradeNoResult := strings.TrimSpace(rsp.TerminalTrace)
if tradeNoResult == "" {
tradeNoResult = strings.TrimSpace(rsp.PayTrace)
}
if tradeNoResult == "" {
return nil, errors.New("扫呗支付查单响应缺少商户订单号")
}
status := normalizeSaobeiState(rsp.TradeState)
if strings.TrimSpace(rsp.ResultCode) == saobei.ResultCodePaying {
status = "pending"
}
result := &bizpayment.PaymentResult{Provider: bizpayment.PaymentSaobei, Status: status, TradeNo: tradeNoResult, ProviderTradeNo: strings.TrimSpace(rsp.ChannelTradeNo), QueryID: queryID, Payload: mustJSON(rsp), Currency: strings.ToUpper(firstAnyOr(c, "CNY", "currency", "default_currency"))}
if rsp.TotalFee != "" {
result.Amount, err = parseIntegerAmount(rsp.TotalFee)
if err != nil {
return nil, fmt.Errorf("解析扫呗订单金额: %w", err)
}
}
if rsp.BuyerPayFee != "" {
result.PayerPaidAmount, err = parseIntegerAmount(rsp.BuyerPayFee)
if err != nil {
return nil, fmt.Errorf("解析扫呗买家实付金额: %w", err)
}
result.CashPaidAmount = result.PayerPaidAmount
if result.Amount >= result.PayerPaidAmount {
result.DiscountAmount = result.Amount - result.PayerPaidAmount
result.AmountBreakdownKnown = true
}
}
return result, err
}
func (a *saobeiAdapter) Refund(ctx context.Context, req *bizpayment.PaymentRefundRequest, c map[string]any) (*bizpayment.PaymentResult, error) {
if req == nil {
return nil, errors.New("扫呗支付退款请求为空")
}
tradeNo := strings.TrimSpace(req.TradeNo)
refundNo := strings.TrimSpace(req.RefundNo)
if tradeNo == "" || refundNo == "" {
return nil, errors.New("扫呗支付退款缺少商户订单号或退款单号")
}
if req.Amount <= 0 {
return nil, errors.New("扫呗支付退款金额无效")
}
client, err := a.client(c)
if err != nil {
return nil, err
}
outTradeNo := strings.TrimSpace(req.QueryID)
if outTradeNo == "" {
outTradeNo = tradeNo
}
bm := gopay.BodyMap{"pay_type": firstAnyOr(c, "010", "pay_type", "payment_type"), "terminal_trace": refundNo, "terminal_time": time.Now().Format("20060102150405"), "refund_fee": req.Amount, "out_trade_no": outTradeNo}
mergeGoPayConfigExtras(bm, c, "refund_extra", "pay_type", "terminal_trace", "terminal_time", "refund_fee", "out_trade_no", "out_refund_no")
rsp, err := client.Refund(ctx, bm)
if err != nil {
return nil, err
}
if rsp == nil {
return nil, errors.New("扫呗支付退款响应为空")
}
if !saobeiResultOK(rsp.ReturnCode, rsp.ResultCode) {
return nil, fmt.Errorf("扫呗支付退款失败: %s", firstNonEmpty(rspMap(rsp)["return_msg"], rspMap(rsp)["result_code"]))
}
if responseOrderID := strings.TrimSpace(rsp.OutTradeNo); responseOrderID != "" && responseOrderID != outTradeNo {
return nil, errors.New("扫呗支付退款响应平台订单号不匹配")
}
returnedTrace := strings.TrimSpace(rsp.TerminalTrace)
if returnedTrace == "" {
return nil, errors.New("扫呗支付退款响应缺少 terminal_trace")
}
if returnedTrace != refundNo {
return nil, errors.New("扫呗支付退款响应的 terminal_trace 不匹配")
}
providerRefundID := strings.TrimSpace(rsp.OutRefundNo)
if providerRefundID == "" {
return nil, errors.New("扫呗支付退款响应缺少 out_refund_no")
}
refundAmount, err := parseIntegerAmount(rsp.RefundFee)
if err != nil {
return nil, fmt.Errorf("解析扫呗退款金额: %w", err)
}
if refundAmount != req.Amount {
return nil, errors.New("扫呗支付退款响应金额不匹配")
}
return &bizpayment.PaymentResult{Provider: bizpayment.PaymentSaobei, Status: "pending", TradeNo: tradeNo, ProviderTradeNo: providerRefundID, Amount: refundAmount, Currency: strings.ToUpper(req.Currency), Payload: mustJSON(rsp)}, nil
}
func (a *saobeiAdapter) Callback(_ context.Context, _ *bizpayment.PaymentCallback, _ map[string]any) (*bizpayment.PaymentResult, error) {
return nil, errors.New("扫呗支付回调没有可复用的 GoPay 验签器请改用主动查单")
}