GO App Store Server API Apple 内购订单查询

2,464 阅读2分钟

背景

由于苹果后台没法查看具体每单的支付状态,不像支付宝,微信,谷歌,stripe那样有完善的后台,经常用户反馈会员没加上,这时候就需要根据用户的订单号或订阅id去查询支付状态

准备工作

  1. 登录appstoreconnect.apple.com/
  2. 生成密钥:- 用户和访问 -> 密钥 ->App 内购项目
  3. 创建密钥:复制 Issuer ID密钥 ID
  4. 下载p8格式的密钥,只能下载一次
  5. 转.p8格式为.pem 的密钥, openssl pkcs8 -nocrypt -in ./SubscriptionKey_xx.p8 -out ./sub_pkcs8.pem

Api 封装

参考:developer.apple.com/documentati…

import (
	"bytes"
	"crypto/ecdsa"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"time"

	"github.com/SermoDigital/jose"
	"github.com/dgrijalva/jwt-go"
)

type AppStoreApi struct {
        kid         string  // App 内购买项目 上面步骤3的 密钥ID
	iss         string  // App 内购买项目 上面步骤3的 Issuer ID
	bid         string  // app 包名 如com.alibaba.org
	privatePath string  // 上面步骤5 转格式后的pem 密钥路径
	token       string
	baseApiHost string
}

// 生成Token
func (a *AppStoreApi) getToken() (string, error) {
	token := &jwt.Token{
		Header: map[string]interface{}{
			"typ": "JWT",
			"kid": a.kid,
			"alg": jwt.SigningMethodES256.Alg(),
		},
		Claims: jwt.MapClaims{
			"iss": a.iss,
			"iat": time.Now().Unix(),
			"exp": time.Now().Add(3600 * time.Second).Unix(),
			"aud": "appstoreconnect-v1",
			"bid": a.bid,
		},
		Method: jwt.SigningMethodES256,
	}

	privatePem, err := ioutil.ReadFile(a.privatePath)

	// Parse PEM block
	var block *pem.Block
	if block, _ = pem.Decode(privatePem); block == nil {
		log.Println("must .p8 PEM file")
		return "", errors.New("token: AuthKey must be a valid .p8 PEM file")
	}

	// Parse the key
	var parsedKey interface{}
	if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
		log.Println("==>", err)
		return "", err
	}

	var ecdsaKey *ecdsa.PrivateKey
	var ok bool
	if ecdsaKey, ok = parsedKey.(*ecdsa.PrivateKey); !ok {
		log.Println("must ecdsa.PrivateKey")
		return "", errors.New("token: AuthKey must be of type ecdsa.PrivateKey")
	}

	if err != nil {
		log.Println("ecdsaKey Error...", err)
		return "", err
	}
	tk, err := token.SignedString(ecdsaKey)
	if err != nil {
		log.Println("ecdsaKey Error...", err)
		return "", err
	}
	a.token = tk
	return tk, err
}

// GetAppStoreOrder 查询用户订单的收据 orderId 消费者端的订单号id 如 `MQ1MGKZX31`,
func (a *AppStoreApi) GetAppStoreOrder(orderId string) (resp *InAppLookupResp, err error) {
	apiUri := fmt.Sprintf("%s/inApps/v1/lookup/%s", a.baseApiHost, orderId)

	req, _ := http.NewRequest("GET", apiUri, nil)
	var apiResp struct {
		Status             int64    `json:"status"` // 0: 有效 1: 无效
		SignedTransactions []string `json:"signedTransactions"`
	}
	err = a.doReq(req, &apiResp)
	if err != nil || apiResp.Status == 1 {
		return &InAppLookupResp{}, err
	}
	resp, err = a.DecodeJWSTransaction([]byte(apiResp.SignedTransactions[0]))
	return
}

// 查询用户最后一次订阅项目状态
func (a *AppStoreApi) GetSubscriptionsStatus(originalTransactionId string) (resp []SubscriptionGroupIdentifierItem, err error) {
	apiUri := fmt.Sprintf("%s/inApps/v1/subscriptions/%s", a.baseApiHost, originalTransactionId)
	req, _ := http.NewRequest("GET", apiUri, nil)
	var apiResp struct {
		AppAppleId  int64                             `json:"appAppleId"`
		Environment string                            `json:"environment"`
		BundleId    string                            `json:"bundleId"`
		Data        []SubscriptionGroupIdentifierItem `json:"data"`
	}
	err = a.doReq(req, &apiResp)
	if err != nil {
		return []SubscriptionGroupIdentifierItem{}, err
	}
	return apiResp.Data, nil
}

func (a *AppStoreApi) DecodeJWSTransaction(jwsToken []byte) (inappOrder *InAppLookupResp, err error) {
	parts := bytes.Split(jwsToken, []byte{'.'})
	// todo: parts[0] 为header 用作验证jwt, 暂未验证
	dec, err := jose.Base64Decode(parts[1])
	log.Println("解码64", string(dec))
	err = json.Unmarshal(dec, &inappOrder)
	return
}

func (a *AppStoreApi) doReq(req *http.Request, out interface{}) error {
	token, _ := a.getToken()
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 15 * time.Second}
	var resp *http.Response
	var err error
	for i := 0; i < 3; i++ {
		resp, err = client.Do(req)
		if err != nil {
			if i >= 2 {
				return err
			}

			time.Sleep(500 * time.Millisecond)
			continue
		} else {
			break
		}
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	log.Println("json数据", string(body), err)
	if err != nil || resp.StatusCode != 200 {
		return err
	}
	err = json.Unmarshal(body, &out)
	return err
}

使用


appStoreCli := &AppStoreApi{
		kid: "ZWxxx", // 密钥类型 App内购买项目 P5R8Q8ZN8A
		iss: "547aa60e-xx-xx-xx-xxxx", // App Store Connect API: Issuser ID
		bid: "应用包名(套装 ID)",
		privatePath: "./sub_siphoto_pkcs8.pem",
		baseApiHost: "https://api.storekit.itunes.apple.com",
	}
// 根据用户收据的订单号查询状态
resp, err := appStoreCli.GetAppStoreOrder("MLY76LM2YK")
log.Println(resp, err)

// 根据订阅id 查询订阅状态
resp, err = appStoreCli.GetSubscriptionsStatus("420001389948529")