Golang内购接入流程和解析(GooglePlay,AppleIAP)

简介

项目中使用的是 Google Play 和 Appstore IAP

Google Play采用了 支付完成后由Google服务器通知游戏服务器 的方式,游戏服务器提供的接口必须为 https,请求类型为 post

Appstore 采用了客户端给游戏服务器收据,之后服务器自己拿着 收据向第三方服务器验证 的方式, (只有订阅商品可以走苹果的通知)

使用 go 库

url: https://github.com/awa/go-iap
title: "GitHub - awa/go-iap: go-iap verifies the purchase receipt via AppStore, GooglePlayStore, AmazonAppStore and Huawei HMS."
description: "go-iap verifies the purchase receipt via AppStore, GooglePlayStore, AmazonAppStore and Huawei HMS. - GitHub - awa/go-iap: go-iap verifies the purchase receipt via AppStore, GooglePlayStore, AmazonA..."
host: github.com
favicon: https://github.githubassets.com/favicons/favicon.svg
image: https://opengraph.githubassets.com/082874903e0ef0aef0e79326afb5af592c8b90cf84aff0bb1dc6a0e2cb379311/awa/go-iap

Google Play

文档

url: https://developer.android.com/google/play/billing/rtdn-reference?hl=zh-cn&authuser=1
title: "实时开发者通知参考指南  |  Google Play 结算系统  |  Android Developers"
host: developer.android.com
image: https://developer.android.com/static/images/social/android-developers.png?authuser=1&hl=zh-cn
url: https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products/get?hl=zh-cn
title: "Method: purchases.products.get  |  Google Play Developer API  |  Google for Developers"
host: developers.google.com
image: https://www.gstatic.com/devrel-devsite/prod/vf713985d8e62ba7506345995097e4b76a060f9dc558a369e9b889efae740fb5f/developers/images/opengraph/google-blue.png

配置流程

  1. google cloudAPI 和服务 配置 服务账号,下载 google 认证文件
  2. Pub/Sub 页面,创建 主题和订阅,订阅里设置传送类型为 推送,同时填写要通知的服务器的 URL
  3. 为主题和订阅添加权限 Pub/Sub Admin,账号为 google-play-developer-notifications@system.gserviceaccount.com
  4. google play console,用开发者账户 (项目创建者,需支付 25 刀) ,在 API 权限里启用 Google Play Developer API
  5. 选择程序,在 营利设定 里可以找到 Google Play 付款服务,把之前创建的 主题 填上去,发送测试通知
  6. 如果成功的话服务器将会收到一条测试消息,格式为 json,其中 data 字段被 base64 加密过,需要解码之后查看内容

相关代码

解析消息
type GooglePlayData struct {  
   Version                    string                      `json:"version"`  
   PackageName                string                      `json:"packageName"`  
   EventTimeMillis            string                      `json:"eventTimeMillis"`  
   OneTimeProductNotification *OneTimeProductNotification `json:"oneTimeProductNotification,omitempty"`  
   SubscriptionNotification   *SubscriptionNotification   `json:"subscriptionNotification,omitempty"`  
   TestNotification           *TestNotification           `json:"testNotification,omitempty"`  
}  
  
type SubscriptionNotification struct {  
   Version          string `json:"version"`  
   NotificationType int    `json:"notificationType"`  
   PurchaseToken    string `json:"purchaseToken"`  
   SubscriptionId   string `json:"subscriptionId"`  
}  
  
type OneTimeProductNotification struct {  
   Version          string `json:"version"`  
   NotificationType int    `json:"notificationType"`  
   PurchaseToken    string `json:"purchaseToken"`  
   Sku              string `json:"sku"`  
}  
  
type TestNotification struct {  
   Version string `json:"version"`  
}

-------------------------------------------------------------------------------------------------------------------------------------------------

decodeString, err := base64.StdEncoding.DecodeString("data字段")  
if err != nil {  
   return nil, err  
}  
  
var gpd GooglePlayData  
err = json.Unmarshal(decodeString, &gpd)  
if err != nil {  
   return nil, err  
}
认证
GoogleClient, err = playstore.New("key文件")  
if err != nil {  
   log.Fatal(err)  
}

// 获取订单信息
GoogleClient.VerifyProduct(context.Background(), PackageName, productId, token)
// 确认订单
GoogleClient.AcknowledgeProduct(ctx, PackageName, ProductId, PurchaseToken, DeveloperPayload)
// 消费订单
GoogleClient.ConsumeProduct(ctx, PackageName, ProductId, PurchaseToken)

踩坑

  • 不是所有订单都会触发通知,只有当购买交易从 PENDING 过渡到 PURCHASED 时,应用才会收到 ONE_TIME_PRODUCT_PURCHASED 通知.如果购买交易被取消,应用会收到 ONE_TIME_PRODUCT_CANCELED 通知.也就是说,在用测试卡进行支付测试时,如果选择 " 一律批准 " 的交易方式,就不会触发通知,因为没有触发 pending 的过渡.
  • 使用测试订单进行支付流程的验证时,使用 VerifyProduct 获取到订单信息,里边的 purchaseToken 是空的,进而导致请求时报错,所以要 使用通知消息内带来的 token.

Appstore IAP

文档

适用于苹果订阅

url: https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2decodedpayload
title: "responseBodyV2DecodedPayload | Apple Developer Documentation"
description: "A decoded payload containing the version 2 notification data."
host: developer.apple.com
favicon: /favicon.ico
image: https://docs.developer.apple.com/tutorials/developer-og.jpg

配置流程

  1. app store connection 创建 App 内购买项目 的密钥,下载 下来保存,名称为 SubscriptionKey_*.p8

以下流程为订阅类型商品适用:

  1. 用管理账户在 APP 内的 App 信息 页面,在 App Store 服务器通知 填写生产和沙盒服务器的 URL
  2. 发送一个测试消息,需要设置 JWT 请求头,用的是第 1 步设置的密钥,详情点 这里
  3. 如果成功的话,服务器将会收到收到一条 json 格式的 testNotificationToken,内容为 JWT 加密后的一串数据 signedPayload,解码过后可以看到一些 数据

以下流程为消耗型商品适用:

  1. 客户端收到苹果的支付成功的回调后,将 TransactionId 传给服务端
  2. 服务端拿着 TransactionId 去 App store 服务器获取订单信息,验证 transactionId 是否一致等操作判断订单合法性

相关代码

发起请求
url: https://developer.apple.com/documentation/appstoreserverapi/generating_tokens_for_api_requests
title: "Generating tokens for API requests | Apple Developer Documentation"
description: "Create JSON Web Tokens signed with your private key to authorize App Store Server API requests."
host: developer.apple.com
favicon: /favicon.ico
image: https://docs.developer.apple.com/tutorials/developer-og.jpg

所有请求都需要一个 Bearer Token, 具体的加密方式为

  1. 定义 JWT HeaderJWT Payload
  2. 使用密钥对 JWT对象 进行加密,方式为 ES256,得到 加密字符串
  3. 之后在请求头中加入 Authorization,值为 Bearer 加密字符串

demo:

package main

import (
    "crypto/ecdsa"
    "crypto/x509"
    "encoding/pem"
    "errors"
    "fmt"
    "os"
    "sync"
    "time"
    "github.com/golang-jwt/jwt/v4"
    "github.com/google/uuid")

func main() {
    key, err := os.ReadFile("./SubscriptionKey_WRY3WAF666.p8")
    if err != nil {
       panic(err)
    }

    c := &StoreConfig{  
       KeyContent: key,                                    // Loads a .p8 certificate  
       KeyID:      "2X9R4HXF34",                           // Your private key ID from App Store Connect (Ex: 2X9R4HXF34)  
       BundleID:   "com.xxx.xxx",                          // Your app’s bundle ID  
       Issuer:     "93fe111e-7a12-426b-bb79-53ce456c183e", // Your issuer ID from the Keys page in App Store Connect (Ex: "57246542-96fe-1a63-e053-0824d011072a")  
       Sandbox:    true,                                   // default is Production  
    }  
  
    t := Token{}  
  
    t.WithConfig(c)  
  
    err = t.Generate()  
    if err != nil {  
       panic(err)  
    }  
  
    fmt.Println(t.Bearer)  // 生成的TOKEN

}

type Token struct {
    sync.Mutex

    KeyContent []byte // Loads a .p8 certificate  
    KeyID      string // Your private key ID from App Store Connect (Ex: 2X9R4HXF34)  
    BundleID   string // Your app’s bundle ID  
    Issuer     string // Your issuer ID from the Keys page in App Store Connect (Ex: "57246542-96fe-1a63-e053-0824d011072a")  
    Sandbox    bool   // default is Production  
  
    // internal variables    AuthKey   *ecdsa.PrivateKey // .p8 private key  
    ExpiredAt int64             // The token’s expiration time, in UNIX time. Tokens that expire more than 60 minutes after the time in iat are not valid (Ex: 1623086400)  
    Bearer    string            // Authorized bearer token  

}

type StoreConfig struct {
    KeyContent []byte // Loads a .p8 certificate
    KeyID      string // Your private key ID from App Store Connect (Ex: 2X9R4HXF34)
    BundleID   string // Your app’s bundle ID
    Issuer     string // Your issuer ID from the Keys page in App Store Connect (Ex: "57246542-96fe-1a63-e053-0824d011072a")
    Sandbox    bool   // default is Production
}

func (t *Token) WithConfig(c *StoreConfig) {
    t.KeyContent = append(t.KeyContent[:0:0], c.KeyContent…)
    t.KeyID = c.KeyID
    t.BundleID = c.BundleID
    t.Issuer = c.Issuer
    t.Sandbox = c.Sandbox
}

func (t *Token) Generate() error {
    key, err := t.passKeyFromByte(t.KeyContent)
    if err != nil {
       return err
    }
    t.AuthKey = key

    issuedAt := time.Now().Unix()  
    expiredAt := time.Now().Add(time.Duration(1) * time.Hour).Unix()  
    jwtToken := &jwt.Token{  
       Header: map[string]interface{}{  
          "alg": "ES256",  
          "kid": t.KeyID,  
          "typ": "JWT",  
       },  
  
       Claims: jwt.MapClaims{  
          "iss":   t.Issuer,  
          "iat":   issuedAt,  
          "exp":   expiredAt,  
          "aud":   "appstoreconnect-v1",  
          "nonce": uuid.New(),  
          "bid":   t.BundleID,  
       },  
       Method: jwt.SigningMethodES256,  
    }  
  
    bearer, err := jwtToken.SignedString(t.AuthKey)  
    if err != nil {  
       return err  
    }  
    t.ExpiredAt = expiredAt  
    t.Bearer = bearer  
  
    return nil  

}

// passKeyFromByte loads a .p8 certificate from an in memory byte array and returns an *ecdsa.PrivateKey.
func (t *Token) passKeyFromByte(bytes []byte) (*ecdsa.PrivateKey, error) {
    block, _ := pem.Decode(bytes)
    if block == nil {  
       return nil, ErrAuthKeyInvalidPem  
    }  
  
    key, err := x509.ParsePKCS8PrivateKey(block.Bytes)  
    if err != nil {  
       return nil, err  
    }  
  
    switch pk := key.(type) {  
    case *ecdsa.PrivateKey:  
       return pk, nil  
    default:  
       return nil, ErrAuthKeyInvalidType  
    }  
}  
  
var (  
    ErrAuthKeyInvalidPem  = errors.New("token: AuthKey must be a valid .p8 PEM file")  
    ErrAuthKeyInvalidType = errors.New("token: AuthKey must be of type ecdsa.PrivateKey")  
)
解析 signedTransactionInfo
  1. 分成三部分, 以 . 分开, 分别是 header, payload, signature
  2. 如果只想查看数据, 只需要用 无填充BASE64 解码 payload 即可,详情
  3. header无填充BASE64 解码后, 里边有 algx5c,用来验证签名,详情
  4. 证书地址 https://www.apple.com/certificateauthority/,一般是用 Apple Root CA - G3 Root
解析 signedPayload

适用于订阅商品

type PlatformAppleSubscribeIAP struct {  
   SignedPayload string      `json:"signedPayload"`  
   PayloadData   PayloadData `json:"payloadData"`  
}

type PayloadData struct {  
   NotificationType string              `json:"notificationType"`  //App Store 发送此版本 2 通知的应用内购买事件。  
   NotificationUUID string              `json:"notificationUUID"`  //通知的唯一标识符。使用此值来识别重复的通知。  
   Version          string              `json:"version"`           //App Store 服务器通知版本号,."2.0"  
   SignedDate       float64             `json:"signedDate"`        //App Store 签署 JSON Web 签名数据的 UNIX 时间(以毫秒为单位)。  
   Subtype          string              `json:"subtype"`           //标识通知事件的附加信息。该subtype字段仅针对特定版本 2 通知出现。  
   Summary          *Summary            `json:"summary,omitempty"` //当 App Store 服务器完成您为符合条件的订阅者延长订阅续订日期的请求时显示的摘要数据.与data互斥  
   Data             *Data               `json:"data,omitempty"`    //包含应用程序元数据以及签名的续订和交易信息的对象。与summary互斥  
}

type Summary struct {  
   RequestIdentifier      string   `json:"requestIdentifier,omitempty"`  
   Environment            string   `json:"environment,omitempty"`  
   AppAppleId             int64    `json:"appAppleId,omitempty"`  
   BundleId               string   `json:"bundleId,omitempty"`  
   ProductId              string   `json:"productId,omitempty"`  
   StorefrontCountryCodes []string `json:"storefrontCountryCodes,omitempty"`  
   FailedCount            int64    `json:"failedCount,omitempty"`  
   SucceededCount         int64    `json:"succeededCount,omitempty"`  
}  
  
type Data struct {  
   AppAppleId            int64  `json:"appAppleId,omitempty"`  
   BundleId              string `json:"bundleId,omitempty"`  
   BundleVersion         string `json:"bundleVersion,omitempty"`  
   Environment           string `json:"environment,omitempty"`  
   SignedRenewalInfo     string `json:"signedRenewalInfo,omitempty"`  
   SignedTransactionInfo string `json:"signedTransactionInfo,omitempty"`  
   Status                int32  `json:"status,omitempty"`  
}

AppleParseClient = appstore.New()
token := jwt.Token{}  
err := AppleParseClient.ParseNotificationV2(p.SignedPayload, &token)  
if err != nil {  
   return nil, err  
}  

bytes, err := json.Marshal(token.Claims.(jwt.MapClaims))  
if err != nil {  
   return nil, err  
}  
  
err = json.Unmarshal(bytes, &p.PayloadData)  
if err != nil {  
   return nil, err  
}
验证 transaction

消耗型商品和订阅商品通用

key, err := os.ReadFile(fmt.Sprintf("configs/SubscriptionKey_%s.p8", configs.GetCfg().Appleiap.Kid))  
if err != nil {  
   panic(err)  
}  
  
c := &api.StoreConfig{  
   KeyContent: key,                                 // Loads a .p8 certificate  
   KeyID:      configs.GetCfg().Appleiap.Kid,       // Your private key ID from App Store Connect (Ex: 2X9R4HXF34)  
   BundleID:   configs.GetCfg().Appleiap.Bid,       // Your app’s bundle ID  
   Issuer:     configs.GetCfg().Appleiap.Iss,       // Your issuer ID from the Keys page in App Store Connect (Ex: "57246542-96fe-1a63-e053-0824d011072a")  
   Sandbox:    configs.GetCfg().Appleiap.IsSandbox, // default is Production  
}  

AppleClient := api.NewStoreClient(c)

// 消耗型商品
res, _ := AppleClient.GetTransactionInfo(ctx, p.TransactionId) // 需要先去获取transaction信息


// 通用
// 订阅型商品:苹果通知会直接把SignedTransactionInfo信息带过来,可以直接解析
// 消耗型商品:拿到上一步的transaction信息中的SignedTransactionInfo解析
transaction, err := AppleClient.ParseSignedTransaction(SignedTransactionInfo)

if transaction.TransactionID == p.TransactionId {
	// valid
}


  • 27
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要将Golang接入ChatGPT,您可以使用AI的GPT-3 API来实现。以下是一些步骤和示例代码***一步骤。 2. 在Golang中,您可以使用HTTP请求库来与GPT-3 API进行通信。一个常用的库是`net/http`。 3. 在您的代码中,您需要构建一个HTTP POST请求,将您的输入文本发送给GPT-3 API,并接收返回的响应。 下面是一个简单的示例代码,演示如何使用Golang发送请求并接收响应: ```go package main import ( "fmt" "io/ioutil" "net/http" "strings" ) func main() { apiKey := "YOUR_API_KEY" url := "https://api.openai.com/v1/engines/davinci-codex/completions" input := "你想要问的问题" payload := strings.NewReader(fmt.Sprintf(`{ "prompt": "%s", "max_tokens": 50 }`, input)) req, _ := http.NewRequest("POST", url, payload) req.Header.Add("Content-Type", "application/json") req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiKey)) res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := ioutil.ReadAll(res.Body) fmt.Println(string(body)) } ``` 请确保将`YOUR_API_KEY`替换为您在OpenAI网站上获取的API密钥。 4. 在上述代码中,我们使用了`davinci-codex`引擎,您可以根据您需求选择其他引擎。`max_tokens`参数用于指定生成的响应的最大长度。 5. 运行代码后,您将收到来自GPT-3 API的响应,其中包含生成的文本。 这是一个简单的示例,您可以根据自己的需求进行修改和扩展。请确保遵循OpenAI的使用政策和指导方针。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值