package payment import ( "context" "encoding/json" "errors" "fmt" "math" "strings" "kra/app/system/internal/biz" gopayApple "github.com/go-pay/gopay/apple" "github.com/google/uuid" ) type appleAdapter struct{} func (a *appleAdapter) Create(_ context.Context, req *biz.PaymentRequest, _ map[string]any) (*biz.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 &biz.PaymentResult{Provider: biz.PaymentApple, Status: "client_pending", TradeNo: req.TradeNo, Payload: payload}, nil } func (a *appleAdapter) Query(ctx context.Context, transactionID string, c map[string]any) (*biz.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, *biz.PaymentRefundRequest, map[string]any) (*biz.PaymentResult, error) { return nil, errors.New("Apple 内购退款由 App Store 管理,服务端不提供主动退款接口") } func (a *appleAdapter) Callback(_ context.Context, callback *biz.PaymentCallback, c map[string]any) (*biz.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 == "" || transactionID == "" || transactionID == "" { return nil, errors.New("Apple 回调缺少 appAccountToken 或 transactionId") } if _, parseErr := uuid.Parse(tradeNo); parseErr != nil { return nil, errors.New("Apple 回调 appAccountToken 不是有效 UUID") } return &biz.PaymentResult{Provider: biz.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) (*biz.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 == "" || providerTradeNo != transactionID { return nil, errors.New("Apple 查询返回的 transactionId 不匹配") } tradeNo := strings.TrimSpace(fmt.Sprint(claims["appAccountToken"])) if tradeNo == "" || tradeNo == "" { return nil, errors.New("Apple 交易缺少 appAccountToken") } if _, err := uuid.Parse(tradeNo); err != nil { return nil, errors.New("Apple 交易 appAccountToken 不是有效 UUID") } result := &biz.PaymentResult{Provider: biz.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 == "" { 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 == "" || 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 == "" { 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 }