package main // Apple App Store Server API 测试脚本 // 用法: go run scripts/test_apple_lookup.go // 功能: 通过 Apple Server API 获取交易历史,并按 appAccountToken (UUID) 过滤匹配交易 import ( "bytes" "crypto/ecdsa" "crypto/rand" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "io" "net/http" "os" "strings" "time" ) // ==================== 配置区域 ==================== // 请填入你的 Apple App Store Connect API 凭证 const ( keyID = "" // App Store Connect Key ID issuerID = "" // App Store Connect Issuer ID bundleID = "com.hifastvpn.vip" // 你的 App Bundle ID sandbox = true // true=沙盒环境, false=生产环境 ) // 私钥内容 (PEM 格式) // 从 App Store Connect 下载的 .p8 文件内容 var privateKeyPEM = `` // ==================== 主逻辑 ==================== func main() { if len(os.Args) < 2 { fmt.Println("用法:") fmt.Println(" go run scripts/test_apple_lookup.go [appAccountToken]") fmt.Println("") fmt.Println("参数:") fmt.Println(" originalTransactionId Apple 交易原始 ID") fmt.Println(" appAccountToken 可选, 用于过滤的 UUID (服务端下单时生成)") fmt.Println("") fmt.Println("示例:") fmt.Println(" go run scripts/test_apple_lookup.go 2000001132940893") fmt.Println(" go run scripts/test_apple_lookup.go 2000001132940893 f0eb8c62-4be9-4be7-9266-58d1a0a4e7bf") os.Exit(1) } originalTransactionId := os.Args[1] filterToken := "" if len(os.Args) >= 3 { filterToken = os.Args[2] } if keyID == "" || issuerID == "" || privateKeyPEM == "" { fmt.Println("❌ 请先在脚本中填入 Apple API 凭证 (keyID, issuerID, privateKeyPEM)") os.Exit(1) } fmt.Println("═══════════════════════════════════════════════") fmt.Println(" Apple App Store Server API - 交易历史查询") fmt.Println("═══════════════════════════════════════════════") fmt.Printf(" 环境: %s\n", envName()) fmt.Printf(" TransactionID: %s\n", originalTransactionId) if filterToken != "" { fmt.Printf(" 过滤 Token: %s\n", filterToken) } fmt.Println("═══════════════════════════════════════════════") // 1. 生成 JWT token, err := buildJWT() if err != nil { fmt.Printf("❌ 生成 JWT 失败: %v\n", err) os.Exit(1) } fmt.Println("✅ JWT 生成成功") // 2. 查询交易历史 fmt.Printf("\n📡 正在查询交易历史...\n") transactions, err := getTransactionHistory(token, originalTransactionId) if err != nil { fmt.Printf("❌ 查询失败: %v\n", err) os.Exit(1) } fmt.Printf("✅ 共获取 %d 条交易记录\n\n", len(transactions)) // 3. 解析并展示交易 for i, jws := range transactions { info, err := parseJWS(jws) if err != nil { fmt.Printf(" [%d] ❌ 解析失败: %v\n", i+1, err) continue } txToken := info["appAccountToken"] matched := "" if filterToken != "" && fmt.Sprintf("%v", txToken) == filterToken { matched = " ✅ 匹配!" } fmt.Printf(" [%d]%s\n", i+1, matched) fmt.Printf(" TransactionID: %v\n", info["transactionId"]) fmt.Printf(" OriginalTxID: %v\n", info["originalTransactionId"]) fmt.Printf(" ProductID: %v\n", info["productId"]) fmt.Printf(" AppAccountToken: %v\n", txToken) fmt.Printf(" PurchaseDate: %v\n", formatTimestamp(info["purchaseDate"])) fmt.Printf(" Type: %v\n", info["type"]) fmt.Println() } // 4. 如果指定了过滤 token,显示匹配结果 if filterToken != "" { found := false for _, jws := range transactions { info, err := parseJWS(jws) if err != nil { continue } if fmt.Sprintf("%v", info["appAccountToken"]) == filterToken { found = true fmt.Println("═══════════════════════════════════════════════") fmt.Printf("🎯 找到匹配的交易!AppAccountToken: %s\n", filterToken) fmt.Printf(" TransactionID: %v\n", info["transactionId"]) fmt.Printf(" ProductID: %v\n", info["productId"]) fmt.Println("═══════════════════════════════════════════════") break } } if !found { fmt.Println("═══════════════════════════════════════════════") fmt.Printf("⚠️ 未找到 AppAccountToken=%s 的交易\n", filterToken) fmt.Println("═══════════════════════════════════════════════") } } } // ==================== Apple API 调用 ==================== // getTransactionHistory 获取指定交易 ID 的完整交易历史 // Apple API: GET /inApps/v2/history/{transactionId} func getTransactionHistory(jwt, transactionId string) ([]string, error) { var allTransactions []string revision := "" for { host := apiHost() url := fmt.Sprintf("%s/inApps/v2/history/%s?sort=DESCENDING", host, transactionId) if revision != "" { url += "&revision=" + revision } req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+jwt) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("HTTP 请求失败: %v", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 { // 尝试另一个环境 host2 := apiHostSecondary() url2 := fmt.Sprintf("%s/inApps/v2/history/%s?sort=DESCENDING", host2, transactionId) if revision != "" { url2 += "&revision=" + revision } req2, _ := http.NewRequest("GET", url2, nil) req2.Header.Set("Authorization", "Bearer "+jwt) resp2, err2 := http.DefaultClient.Do(req2) if err2 != nil { return nil, fmt.Errorf("两个环境都失败: primary[%d:%s]", resp.StatusCode, string(body)) } defer resp2.Body.Close() body2, _ := io.ReadAll(resp2.Body) if resp2.StatusCode != 200 { return nil, fmt.Errorf("两个环境都失败: primary[%d:%s], secondary[%d:%s]", resp.StatusCode, string(body), resp2.StatusCode, string(body2)) } body = body2 } var result struct { SignedTransactions []string `json:"signedTransactions"` Revision string `json:"revision"` HasMore bool `json:"hasMore"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("解析响应失败: %v, body: %s", err, string(body)) } allTransactions = append(allTransactions, result.SignedTransactions...) if !result.HasMore || result.Revision == "" { break } revision = result.Revision } return allTransactions, nil } // ==================== JWT 构建 ==================== // buildJWT 构建 Apple Server API 的 ES256 JWT Token func buildJWT() (string, error) { header := map[string]interface{}{ "alg": "ES256", "kid": keyID, "typ": "JWT", } now := time.Now().Unix() payload := map[string]interface{}{ "iss": issuerID, "iat": now, "exp": now + 1800, "aud": "appstoreconnect-v1", } if bundleID != "" { payload["bid"] = bundleID } hb, _ := json.Marshal(header) pb, _ := json.Marshal(payload) enc := func(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) } unsigned := fmt.Sprintf("%s.%s", enc(hb), enc(pb)) key := fixPEM(privateKeyPEM) block, _ := pem.Decode([]byte(key)) if block == nil { return "", fmt.Errorf("invalid private key PEM") } keyAny, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return "", fmt.Errorf("parse private key failed: %v", err) } priv, ok := keyAny.(*ecdsa.PrivateKey) if !ok { return "", fmt.Errorf("private key is not ECDSA") } h := sha256.New() h.Write([]byte(unsigned)) digest := h.Sum(nil) r, s, err := ecdsa.Sign(rand.Reader, priv, digest) if err != nil { return "", err } curveBits := priv.Curve.Params().BitSize keyBytes := curveBits / 8 if curveBits%8 > 0 { keyBytes++ } rBytes := r.Bytes() rPadded := make([]byte, keyBytes) copy(rPadded[keyBytes-len(rBytes):], rBytes) sBytes := s.Bytes() sPadded := make([]byte, keyBytes) copy(sPadded[keyBytes-len(sBytes):], sBytes) sig := append(rPadded, sPadded...) return unsigned + "." + base64.RawURLEncoding.EncodeToString(sig), nil } // ==================== 工具函数 ==================== // parseJWS 解析 Apple 返回的 JWS (只取 payload 部分) func parseJWS(jws string) (map[string]interface{}, error) { parts := strings.Split(jws, ".") if len(parts) != 3 { return nil, fmt.Errorf("invalid JWS format") } payload, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { // 尝试标准 base64 payload, err = base64.RawStdEncoding.DecodeString(parts[1]) if err != nil { return nil, fmt.Errorf("decode payload failed: %v", err) } } var result map[string]interface{} if err := json.Unmarshal(payload, &result); err != nil { return nil, fmt.Errorf("unmarshal payload failed: %v", err) } return result, nil } // formatTimestamp 格式化 Apple 返回的毫秒时间戳 func formatTimestamp(v interface{}) string { if v == nil { return "N/A" } switch t := v.(type) { case float64: ts := time.UnixMilli(int64(t)) return ts.Format("2006-01-02 15:04:05") default: return fmt.Sprintf("%v", v) } } func fixPEM(key string) string { if !strings.Contains(key, "\n") && strings.Contains(key, "BEGIN PRIVATE KEY") { key = strings.ReplaceAll(key, " ", "\n") key = strings.ReplaceAll(key, "-----BEGIN\nPRIVATE\nKEY-----", "-----BEGIN PRIVATE KEY-----") key = strings.ReplaceAll(key, "-----END\nPRIVATE\nKEY-----", "-----END PRIVATE KEY-----") } return key } func apiHost() string { if sandbox { return "https://api.storekit-sandbox.itunes.apple.com" } return "https://api.storekit.itunes.apple.com" } func apiHostSecondary() string { if sandbox { return "https://api.storekit.itunes.apple.com" } return "https://api.storekit-sandbox.itunes.apple.com" } func envName() string { if sandbox { return "🏖️ Sandbox" } return "🏭 Production" } // 忽略未使用导入 var _ = bytes.NewBuffer