优化结构
This commit is contained in:
@@ -1,269 +0,0 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"kra/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 == "<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 &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 == "<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 := &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 == "<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
|
||||
}
|
||||
Reference in New Issue
Block a user