##苹果支付退款
相信不少的开发伙伴都会遇到支付退款的情况,但是苹果退款和微信及支付宝流程不太一样,苹果退款不需要通过我们自己审核,而是苹果官方去审核,主动权就没在我们手里,针对这种情况,我们该如何处理呢?
首先在苹果开发者中心配置退款的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, ¶m); 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 处理回调的具体代码了,我隐藏了部分代码逻辑,可根据实际情况做调整。苹果回调能够拿到通知类型和交易流水号,和系统中的交易记录做对比,处理相应的业务逻辑~。
后续有问题可以在评论区留言。