13 changed files with 350 additions and 4 deletions
@ -0,0 +1,117 @@
|
||||
package internal |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"fmt" |
||||
"github.com/SermoDigital/jose" |
||||
"github.com/gogf/gf/v2/encoding/gjson" |
||||
"github.com/golang-jwt/jwt/v5" |
||||
"io" |
||||
"log" |
||||
"net/http" |
||||
"strings" |
||||
"time" |
||||
"tyj_admin/api/v1/game" |
||||
) |
||||
|
||||
// Apple JWT 认证配置
|
||||
type AppleAPIConfig struct { |
||||
IssuerID string `json:"issuer_id"` // 苹果开发者团队ID(格式:57246542-96fe-1a63e053-0824d011072a)
|
||||
BundleID string `json:"bundle_id"` // 应用Bundle ID(如:com.example.game)
|
||||
KeyID string `json:"key_id"` // 苹果API密钥ID(格式:2X9R4HXF34)
|
||||
PrivateKey []byte `json:"private_key"` // 从苹果开发者平台下载的.p8私钥文件内容
|
||||
} |
||||
|
||||
// 订单查询响应结构体
|
||||
type AppleOrderResponse struct { |
||||
Status int `json:"status"` // 状态码(0表示成功)
|
||||
SignedTransactions []string `json:"signedTransactions"` |
||||
} |
||||
|
||||
func LoadP8File() []byte { |
||||
return []byte(`-----BEGIN PRIVATE KEY----- |
||||
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg2XqPEHgWj2cUnO2GoPfIeEcAc0tJsTehvNNNBTGf4KigCgYIKoZIzj0DAQehRANCAAT6IdBMPYuNAQYuZsYi3EkflniotI/KJa6ELt1ednywlOpuwgNOn2WXONmDzzVVMJqQjD/6FSJ4jH7fRtP+Eci6 |
||||
-----END PRIVATE KEY-----`) |
||||
} |
||||
|
||||
func GenerateAppleJWT(config AppleAPIConfig) (string, error) { |
||||
// 使用ES256算法签名
|
||||
key, _ := jwt.ParseECPrivateKeyFromPEM(config.PrivateKey) |
||||
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{ |
||||
"iss": config.IssuerID, |
||||
"iat": time.Now().Unix(), |
||||
"exp": time.Now().Add(5 * time.Minute).Unix(), // 有效期5分钟
|
||||
"aud": "appstoreconnect-v1", |
||||
"bid": config.BundleID, |
||||
}) |
||||
token.Header["kid"] = config.KeyID |
||||
return token.SignedString(key) |
||||
} |
||||
|
||||
func QueryAppleOrder(orderID string) (*game.SignedTransaction, error) { |
||||
config := AppleAPIConfig{ |
||||
IssuerID: "b8b82821-922e-4b43-a3cf-d293020a70d1", |
||||
BundleID: "com.XiamenAvatar.PeachValley", |
||||
KeyID: "J4RAWQBLHF", |
||||
PrivateKey: LoadP8File(), |
||||
} |
||||
|
||||
// 构造API地址
|
||||
baseURL := "https://api.storekit.itunes.apple.com/inApps/v1/lookup/" |
||||
if isSandboxOrder(orderID) { // 根据订单号自动判断环境
|
||||
baseURL = strings.Replace(baseURL, "storekit", "storekit-sandbox", 1) |
||||
} |
||||
url := baseURL + orderID |
||||
|
||||
// 生成JWT令牌
|
||||
authToken, err := GenerateAppleJWT(config) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("JWT生成失败: %v", err) |
||||
} |
||||
|
||||
log.Printf("authToken %v", gjson.MustEncodeString(authToken)) |
||||
// 创建HTTP请求
|
||||
req, _ := http.NewRequest("GET", url, nil) |
||||
req.Header.Set("Authorization", "Bearer "+authToken) |
||||
req.Header.Set("Content-Type", "application/json") |
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second} |
||||
resp, err := client.Do(req) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("API请求失败: %v", err) |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
// 解析响应
|
||||
if resp.StatusCode != http.StatusOK { |
||||
body, _ := io.ReadAll(resp.Body) |
||||
return nil, fmt.Errorf("苹果接口异常[%d]: %s", resp.StatusCode, string(body)) |
||||
} |
||||
var result AppleOrderResponse |
||||
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { |
||||
return nil, err |
||||
} |
||||
if result.Status != 0 { |
||||
return nil, fmt.Errorf("订单查询失败,状态码:%d", result.Status) |
||||
} |
||||
|
||||
data, err := DecodeJWSTransaction([]byte(result.SignedTransactions[0])) |
||||
log.Printf("result %v", gjson.MustEncodeString(data)) |
||||
return data, err |
||||
} |
||||
|
||||
func DecodeJWSTransaction(jwsToken []byte) (inappOrder *game.SignedTransaction, 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 isSandboxOrder(orderID string) bool { |
||||
return strings.HasPrefix(orderID, "SANDBOX_") || |
||||
strings.HasPrefix(orderID, "TEST_") |
||||
} |
@ -0,0 +1,3 @@
|
||||
`-----BEGIN PRIVATE KEY----- |
||||
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg2XqPEHgWj2cUnO2GoPfIeEcAc0tJsTehvNNNBTGf4KigCgYIKoZIzj0DAQehRANCAAT6IdBMPYuNAQYuZsYi3EkflniotI/KJa6ELt1ednywlOpuwgNOn2WXONmDzzVVMJqQjD/6FSJ4jH7fRtP+Eci6 |
||||
-----END PRIVATE KEY-----` |
Loading…
Reference in new issue