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