269 lines
8.9 KiB
Go
269 lines
8.9 KiB
Go
package payment
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
bizpayment "kra/internal/biz/payment"
|
|
"math"
|
|
"strings"
|
|
|
|
gopayApple "github.com/go-pay/gopay/apple"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type appleAdapter struct{}
|
|
|
|
func (a *appleAdapter) Create(_ context.Context, req *bizpayment.PaymentRequest, _ map[string]any) (*bizpayment.PaymentResult, error) {
|
|
if req == nil {
|
|
return nil, errors.New("Apple 内购下单参数为空")
|
|
}
|
|
productID := ""
|
|
if req.Extra != nil {
|
|
productID, _ = req.Extra["product_id"].(string)
|
|
}
|
|
if productID == "" {
|
|
return nil, errors.New("Apple 内购缺少 extra.product_id")
|
|
}
|
|
if _, err := uuid.Parse(req.TradeNo); err != nil {
|
|
return nil, errors.New("Apple 内购 tradeNo 必须是用于 appAccountToken 的 UUID")
|
|
}
|
|
payload, _ := json.Marshal(map[string]any{"product_id": productID, "app_account_token": req.TradeNo, "client_only": true, "trade_no": req.TradeNo})
|
|
return &bizpayment.PaymentResult{Provider: bizpayment.PaymentApple, Status: "client_pending", TradeNo: req.TradeNo, Payload: payload}, nil
|
|
}
|
|
func (a *appleAdapter) Query(ctx context.Context, transactionID string, c map[string]any) (*bizpayment.PaymentResult, error) {
|
|
transactionID = strings.TrimSpace(transactionID)
|
|
if transactionID == "" {
|
|
return nil, errors.New("Apple 查询缺少 transactionId")
|
|
}
|
|
client, err := gopayApple.NewClient(
|
|
text(c, "issuer_id"),
|
|
text(c, "bundle_id"),
|
|
text(c, "key_id"),
|
|
text(c, "private_key"),
|
|
!strings.EqualFold(text(c, "environment"), "sandbox"),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
response, err := client.GetTransactionInfo(ctx, transactionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if response == nil || response.SignedTransactionInfo == "" {
|
|
return nil, errors.New("Apple 查询响应缺少 signedTransactionInfo")
|
|
}
|
|
if err = validateAppleJWSChain(response.SignedTransactionInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
transaction, err := response.DecodeSignedTransaction()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
claims, err := appleClaims(transaction)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bundleID := text(c, "bundle_id"); bundleID != "" && fmt.Sprint(claims["bundleId"]) != bundleID {
|
|
return nil, errors.New("Apple 交易 bundleId 不匹配")
|
|
}
|
|
if err = validateAppleEnvironment(claims, c); err != nil {
|
|
return nil, err
|
|
}
|
|
payload, _ := json.Marshal(claims)
|
|
result, err := applePaymentResult(transactionID, claims, payload, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if result.ProviderTradeNo == "" {
|
|
return nil, errors.New("Apple 交易缺少 transactionId")
|
|
}
|
|
return result, nil
|
|
}
|
|
func (a *appleAdapter) Refund(context.Context, *bizpayment.PaymentRefundRequest, map[string]any) (*bizpayment.PaymentResult, error) {
|
|
return nil, errors.New("Apple 内购退款由 App Store 管理,服务端不提供主动退款接口")
|
|
}
|
|
func (a *appleAdapter) Callback(_ context.Context, callback *bizpayment.PaymentCallback, c map[string]any) (*bizpayment.PaymentResult, error) {
|
|
if callback == nil {
|
|
return nil, errors.New("Apple 回调为空")
|
|
}
|
|
var body struct {
|
|
SignedPayload string `json:"signedPayload"`
|
|
}
|
|
if err := json.Unmarshal(callback.Body, &body); err != nil {
|
|
return nil, err
|
|
}
|
|
if body.SignedPayload == "" {
|
|
return nil, errors.New("Apple 回调缺少 signedPayload")
|
|
}
|
|
if err := validateAppleJWSChain(body.SignedPayload); err != nil {
|
|
return nil, err
|
|
}
|
|
notification, err := gopayApple.DecodeSignedPayload(body.SignedPayload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if notificationID := strings.TrimSpace(notification.NotificationUUID); notificationID != "" {
|
|
if callback.Query == nil {
|
|
callback.Query = map[string]string{}
|
|
}
|
|
callback.Query["event_id"] = notificationID
|
|
}
|
|
if notification.Data == nil {
|
|
return nil, errors.New("Apple 回调缺少 data")
|
|
}
|
|
if err = validateAppleJWSChain(notification.Data.SignedTransactionInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
transaction, err := notification.DecodeTransactionInfo()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
claims, err := appleClaims(transaction)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bundleID := text(c, "bundle_id"); bundleID != "" && fmt.Sprint(claims["bundleId"]) != bundleID {
|
|
return nil, errors.New("Apple 回调 bundleId 不匹配")
|
|
}
|
|
if err = validateAppleEnvironment(claims, c); err != nil {
|
|
return nil, err
|
|
}
|
|
payload, _ := json.Marshal(claims)
|
|
tradeNo := strings.TrimSpace(fmt.Sprint(claims["appAccountToken"]))
|
|
transactionID := strings.TrimSpace(fmt.Sprint(claims["transactionId"]))
|
|
if tradeNo == "" || tradeNo == "<nil>" || transactionID == "" || transactionID == "<nil>" {
|
|
return nil, errors.New("Apple 回调缺少 appAccountToken 或 transactionId")
|
|
}
|
|
if _, parseErr := uuid.Parse(tradeNo); parseErr != nil {
|
|
return nil, errors.New("Apple 回调 appAccountToken 不是有效 UUID")
|
|
}
|
|
return &bizpayment.PaymentResult{Provider: bizpayment.PaymentApple, Status: "pending", TradeNo: tradeNo, ProviderTradeNo: transactionID, QueryID: transactionID, Payload: payload}, nil
|
|
}
|
|
|
|
func appleClaims(value any) (map[string]any, error) {
|
|
payload, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
claims := map[string]any{}
|
|
if err := json.Unmarshal(payload, &claims); err != nil {
|
|
return nil, err
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
func applePaymentResult(transactionID string, claims map[string]any, payload []byte, c map[string]any) (*bizpayment.PaymentResult, error) {
|
|
revoked, err := appleRevoked(claims)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
status := "success"
|
|
if revoked {
|
|
status = "failed"
|
|
}
|
|
providerTradeNo := strings.TrimSpace(fmt.Sprint(claims["transactionId"]))
|
|
if providerTradeNo == "" || providerTradeNo == "<nil>" || providerTradeNo != transactionID {
|
|
return nil, errors.New("Apple 查询返回的 transactionId 不匹配")
|
|
}
|
|
tradeNo := strings.TrimSpace(fmt.Sprint(claims["appAccountToken"]))
|
|
if tradeNo == "" || tradeNo == "<nil>" {
|
|
return nil, errors.New("Apple 交易缺少 appAccountToken")
|
|
}
|
|
if _, err := uuid.Parse(tradeNo); err != nil {
|
|
return nil, errors.New("Apple 交易 appAccountToken 不是有效 UUID")
|
|
}
|
|
result := &bizpayment.PaymentResult{Provider: bizpayment.PaymentApple, Status: status, TradeNo: tradeNo, ProviderTradeNo: providerTradeNo, QueryID: providerTradeNo, Payload: payload}
|
|
if status != "success" {
|
|
return result, nil
|
|
}
|
|
result.Currency = strings.ToUpper(strings.TrimSpace(fmt.Sprint(claims["currency"])))
|
|
if result.Currency == "<nil>" {
|
|
result.Currency = ""
|
|
}
|
|
if result.Currency == "" {
|
|
return nil, errors.New("Apple 交易缺少 currency")
|
|
}
|
|
price, err := parseIntegerAmount(strings.TrimSpace(fmt.Sprint(claims["price"])))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("解析 Apple 交易 price: %w", err)
|
|
}
|
|
divisor := applePriceDivisor(c, result.Currency)
|
|
if divisor <= 0 || price%divisor != 0 {
|
|
return nil, errors.New("Apple price_divisor 未配置或交易金额无法精确换算到最小货币单位")
|
|
}
|
|
result.Amount = price / divisor
|
|
result.PayerPaidAmount = result.Amount
|
|
result.CashPaidAmount = result.Amount
|
|
result.SettlementAmount = result.Amount
|
|
result.PayerCurrency = result.Currency
|
|
result.AmountBreakdownKnown = true
|
|
return result, nil
|
|
}
|
|
|
|
func appleRevoked(claims map[string]any) (bool, error) {
|
|
value, exists := claims["revocationDate"]
|
|
if !exists || value == nil {
|
|
return false, nil
|
|
}
|
|
switch number := value.(type) {
|
|
case float64:
|
|
if math.IsNaN(number) || math.IsInf(number, 0) || number < 0 || math.Trunc(number) != number {
|
|
return false, errors.New("Apple revocationDate 无效")
|
|
}
|
|
return number > 0, nil
|
|
case json.Number:
|
|
parsed, err := number.Int64()
|
|
if err != nil || parsed < 0 {
|
|
return false, errors.New("Apple revocationDate 无效")
|
|
}
|
|
return parsed > 0, nil
|
|
case int64:
|
|
if number < 0 {
|
|
return false, errors.New("Apple revocationDate 无效")
|
|
}
|
|
return number > 0, nil
|
|
case int:
|
|
if number < 0 {
|
|
return false, errors.New("Apple revocationDate 无效")
|
|
}
|
|
return number > 0, nil
|
|
}
|
|
textValue := strings.TrimSpace(fmt.Sprint(value))
|
|
if textValue == "" || textValue == "<nil>" || textValue == "0" {
|
|
return false, nil
|
|
}
|
|
parsed, err := parseIntegerAmount(textValue)
|
|
if err != nil {
|
|
return false, fmt.Errorf("解析 Apple revocationDate: %w", err)
|
|
}
|
|
return parsed > 0, nil
|
|
}
|
|
|
|
func applePriceDivisor(c map[string]any, currency string) int64 {
|
|
if values, ok := c["price_divisors"].(map[string]any); ok {
|
|
for key, value := range values {
|
|
if strings.EqualFold(strings.TrimSpace(key), currency) {
|
|
return configuredInt64(map[string]any{"value": value}, "value", 0)
|
|
}
|
|
}
|
|
}
|
|
return configuredInt64(c, "price_divisor", 0)
|
|
}
|
|
|
|
func validateAppleEnvironment(claims map[string]any, c map[string]any) error {
|
|
actual := strings.ToLower(strings.TrimSpace(fmt.Sprint(claims["environment"])))
|
|
if actual == "" || actual == "<nil>" {
|
|
return errors.New("Apple 交易缺少 environment")
|
|
}
|
|
expected := "production"
|
|
if strings.EqualFold(text(c, "environment"), "sandbox") {
|
|
expected = "sandbox"
|
|
}
|
|
if actual != expected {
|
|
return errors.New("Apple 交易 environment 不匹配")
|
|
}
|
|
return nil
|
|
}
|