苹果支付退款(apple pay),程序如何处理

##苹果支付退款

        相信不少的开发伙伴都会遇到支付退款的情况,但是苹果退款和微信及支付宝流程不太一样,苹果退款不需要通过我们自己审核,而是苹果官方去审核,主动权就没在我们手里,针对这种情况,我们该如何处理呢?

        首先在苹果开发者中心配置退款的url,这里如何配置需要和ios开发的小伙伴去沟通,他们应该清楚,配置好url之后,剩下的事情就是我们服务端的事情了,下面言归正传~

        下面是go处理苹果退款的业务逻辑:

//导入的包
import (
	"context"
	"crypto/ecdsa"
	"crypto/x509"
	"did-planet-server/pkg/logger"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/go-pay/gopay/apple"
	"github.com/go-pay/gopay/pkg/jwt"
	"github.com/go-pay/gopay/pkg/xlog"
	"strings"
)
//这里为了方便查看代码,我把结构体及方法都放到一个文件中便于翻阅,可根据实际开发需求调整
// AppleCallback 苹果支付验证结果
func (s *WalletAPI) AppleCallback(c *gin.Context) {
    //接受苹果退款回调的参数,苹果只返回一个参数signedPayload,这是一个很长的加密字符串
	param := NotificationV2Req{}
	if err := ginx.ParseJSON(c, &param); err != nil {
		ginx.ResError(c, err)
		return
	}
	ctx := c.Request.Context()
    //解析参数,拿到返回的通知类型及解析的交易数据,交易类型分为多种,可查阅官方文档,常见的类型的是refund(退款)类型,rsp可以拿到交易流水号
    //
	notificationType, rsp, err := NotificationV2(param)
	if err != nil {
		xlog.Error(err)
		return
	}
	//todo  处理业务逻辑,具体实现可根据业务调整
	err = s.WalletSrv.ApplePayRefund(ctx, notificationType, rsp)

	if err != nil {
		ginx.ResError(c, err)
		return
	}
	ginx.ResSuccess(c, nil)
	return
}

type NotificationV2Req struct {
	SignedPayload string `json:"signedPayload"`
}


// TransactionInfo https://developer.apple.com/documentation/appstoreservernotifications/jwstransactiondecodedpayload
type TransactionInfo struct {
	jwt.StandardClaims
	AppAccountToken             string `json:"appAccountToken"`
	BundleId                    string `json:"bundleId"`
	Environment                 string `json:"environment"`
	ExpiresDate                 int64  `json:"expiresDate"`
	InAppOwnershipType          string `json:"inAppOwnershipType"` // FAMILY_SHARED  PURCHASED
	IsUpgraded                  bool   `json:"isUpgraded"`
	OfferIdentifier             string `json:"offerIdentifier"`
	OfferType                   int64  `json:"offerType"` // 1:An introductory offer. 2:A promotional offer. 3:An offer with a subscription offer code.
	OriginalPurchaseDate        int64  `json:"originalPurchaseDate"`
	OriginalTransactionId       string `json:"originalTransactionId"`
	ProductId                   string `json:"productId"`
	PurchaseDate                int64  `json:"purchaseDate"`
	Quantity                    int64  `json:"quantity"`
	RevocationDate              int64  `json:"revocationDate"`
	RevocationReason            int    `json:"revocationReason"`
	SignedDate                  int64  `json:"signedDate"` // Auto-Renewable Subscription: An auto-renewable subscription.  Non-Consumable: A non-consumable in-app purchase.  Consumable: A consumable in-app purchase.  Non-Renewing Subscription: A non-renewing subcription.
	SubscriptionGroupIdentifier string `json:"subscriptionGroupIdentifier"`
	TransactionId               string `json:"transactionId"`
	Type                        string `json:"type"`
	WebOrderLineItemId          string `json:"webOrderLineItemId"`
}


//解析交易信息
func DecodeTransactionInfo() (ti *TransactionInfo, err error) {
	if d.Data == nil {
		return nil, fmt.Errorf("data is nil")
	}
	if d.Data.SignedTransactionInfo == "" {
		return nil, fmt.Errorf("data.signedTransactionInfo is empty")
	}
	ti = &TransactionInfo{}
	_, err = ExtractClaims(d.Data.SignedTransactionInfo, ti)
	if err != nil {
		return nil, err
	}
	return
}

// DecodeSignedPayload 解析SignedPayload数据
func DecodeSignedPayload(signedPayload string) (payload *NotificationV2Payload, err error) {
	if signedPayload == "" {
		return nil, fmt.Errorf("signedPayload is empty")
	}
	payload = &NotificationV2Payload{}
	_, err = ExtractClaims(signedPayload, payload)
	if err != nil {
		return nil, err
	}
	return
}

// ExtractClaims 解析jws格式数据
func ExtractClaims(signedPayload string, tran jwt.Claims) (interface{}, error) {
	tokenStr := signedPayload
	rootCertStr, err := extractHeaderByIndex(tokenStr, 2)
	if err != nil {
		return nil, err
	}
	intermediaCertStr, err := extractHeaderByIndex(tokenStr, 1)
	if err != nil {
		return nil, err
	}
	if err = verifyCert(rootCertStr, intermediaCertStr); err != nil {
		return nil, err
	}
	_, err = jwt.ParseWithClaims(tokenStr, tran, func(token *jwt.Token) (interface{}, error) {
		return extractPublicKeyFromToken(tokenStr)
	})
	if err != nil {
		return nil, err
	}
	return tran, nil
}

// DecodeSignedPayload 解析SignedPayload数据
func DecodeSignedPayload(signedPayload string) (payload *NotificationV2Payload, err error) {
	if signedPayload == "" {
		return nil, fmt.Errorf("signedPayload is empty")
	}
	payload = &NotificationV2Payload{}
	_, err = ExtractClaims(signedPayload, payload)
	if err != nil {
		return nil, err
	}
	return
}


//
func NotificationV2(params NotificationV2Req) (string, *TransactionInfo, error) {
	ctx := context.Background()
	payload, err := DecodeSignedPayload(params.SignedPayload)
	if err != nil {
		xlog.Error(err)
		return "", nil, err
	}
	bs1, _ := json.Marshal(payload)
	xlog.Color(xlog.RedBright).Info(string(bs1))


	// decode transactionInfo
	transactionInfo, err := payload.DecodeTransactionInfo()
	if err != nil {
		xlog.Error(err)
		return "", nil, err
	}
	bs2, _ := json.Marshal(transactionInfo)
	logger.WithContext(ctx).Infof("[apple_callback]data.transactionInfo: %s", string(bs2))
	/*
		{
		    "appAccountToken":"",
		    "bundleId":"com.audaos.audarecorder",
		    "expiresDate":1646387196000,
		    "inAppOwnershipType":"PURCHASED",
		    "isUpgraded":false,
		    "offerIdentifier":"",
		    "offerType":0,
		    "originalPurchaseDate":1646046037000,
		    "originalTransactionId":"2000000000842607",
		    "productId":"com.audaos.audarecorder.vip.m2",
		    "purchaseDate":1646387016000,
		    "quantity":1,
		    "revocationDate":0,
		    "revocationReason":"",
		    "signedDate":1646387008254,
		    "subscriptionGroupIdentifier":"20929536",
		    "transactionId":"2000000004047119",
		    "type":"Auto-Renewable Subscription",
		    "webOrderLineItemId":"2000000000302832"
		}
	*/
	return payload.NotificationType, transactionInfo, nil
}


// Per doc: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6
func extractPublicKeyFromToken(tokenStr string) (*ecdsa.PublicKey, error) {
	certStr, err := extractHeaderByIndex(tokenStr, 0)
	if err != nil {
		return nil, err
	}
	cert, err := x509.ParseCertificate(certStr)
	if err != nil {
		return nil, err
	}
	switch pk := cert.PublicKey.(type) {
	case *ecdsa.PublicKey:
		return pk, nil
	default:
		return nil, errors.New("appstore public key must be of type ecdsa.PublicKey")
	}
}

func extractHeaderByIndex(tokenStr string, index int) ([]byte, error) {
	if index > 2 {
		return nil, errors.New("invalid index")
	}
	tokenArr := strings.Split(tokenStr, ".")
	headerByte, err := base64.RawStdEncoding.DecodeString(tokenArr[0])
	if err != nil {
		return nil, err
	}
	type Header struct {
		Alg string   `json:"alg"`
		X5c []string `json:"x5c"`
	}
	header := &Header{}
	err = json.Unmarshal(headerByte, header)
	if err != nil {
		return nil, err
	}
	if len(header.X5c) < index {
		return nil, fmt.Errorf("index[%d] > header.x5c slice len(%d)", index, len(header.X5c))
	}
	certByte, err := base64.StdEncoding.DecodeString(header.X5c[index])
	if err != nil {
		return nil, err
	}
	return certByte, nil
}

func verifyCert(certByte, intermediaCertStr []byte) error {
	roots := x509.NewCertPool()
	ok := roots.AppendCertsFromPEM([]byte(rootPEM)) //这里是自己的证书
	if !ok {
		return errors.New("failed to parse root certificate")
	}
	interCert, err := x509.ParseCertificate(intermediaCertStr)
	if err != nil {
		return errors.New("failed to parse intermedia certificate")
	}
	intermedia := x509.NewCertPool()
	intermedia.AddCert(interCert)
	cert, err := x509.ParseCertificate(certByte)
	if err != nil {
		return err
	}
	opts := x509.VerifyOptions{
		Roots:         roots,
		Intermediates: intermedia,
	}
	_, err = cert.Verify(opts)
	return err
}

        上面就是golang 处理回调的具体代码了,我隐藏了部分代码逻辑,可根据实际情况做调整。苹果回调能够拿到通知类型和交易流水号,和系统中的交易记录做对比,处理相应的业务逻辑~。

        后续有问题可以在评论区留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值