背景
由于苹果后台没法查看具体每单的支付状态,不像支付宝,微信,谷歌,stripe那样有完善的后台,经常用户反馈会员没加上,这时候就需要根据用户的订单号或订阅id去查询支付状态
准备工作
- 登录appstoreconnect.apple.com/
- 生成密钥:- 用户和访问 -> 密钥 ->App 内购项目
- 创建密钥:复制
Issuer ID和密钥 ID - 下载p8格式的密钥,只能下载一次
- 转.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")