From d3541a89aec5e3c7877e02e5021436e72acf7a82 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Wed, 17 Dec 2025 04:00:02 -0800 Subject: [PATCH] =?UTF-8?q?fix(iap):=20=E4=BF=AE=E5=A4=8D=E8=8B=B9?= =?UTF-8?q?=E6=9E=9CIAP=E9=87=8D=E5=A4=8D=E5=A4=84=E7=90=86=E4=BA=A4?= =?UTF-8?q?=E6=98=93=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加对已存在订阅的检查逻辑,避免重复处理相同的苹果IAP交易 添加测试恢复接口的示例代码 --- cmd/test_restore/main.go | 111 ++++++++++++++++++ .../public/iap/apple/restoreHandler.go | 1 - .../iap/apple/attachTransactionLogic.go | 13 ++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 cmd/test_restore/main.go diff --git a/cmd/test_restore/main.go b/cmd/test_restore/main.go new file mode 100644 index 0000000..c07fc70 --- /dev/null +++ b/cmd/test_restore/main.go @@ -0,0 +1,111 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strings" + "time" + + pkgaes "github.com/perfect-panel/server/pkg/aes" +) + +// 替换为您实际的服务器地址 +const BaseURL = "https://api.hifast.biz" + +// 替换为您实际的用户登录 Token (Authorization: Bearer ) +const UserToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJEZXZpY2VJZCI6MzgzLCJMb2dpblR5cGUiOiJkZXZpY2UiLCJTZXNzaW9uSWQiOiIwMTliMmFmZC1jMjUwLTc1YmItODQzMy04NDMyNWVmZGRkMzMiLCJVc2VySWQiOjM4MywiZXhwIjoxNzY2NTU3NjMyLCJpYXQiOjE3NjU5NTI4MzJ9.kkcT4ojXG9qn_aVqMaGqUUXhHcZXHy49k5Vn05Et9OM" + +// 替换为您在后台配置的设备通信密钥 (Security Secret) +const DeviceSecret = "c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx" + +// 替换为您要测试的 Transaction ID +const TestTransactionID = "2000001083238483" + +func main() { + fmt.Println("开始测试 Restore 接口 (AES 加密模式)...") + fmt.Printf("目标 Transaction ID: %s\n", TestTransactionID) + + // 1. 构造原始请求数据 + payload := map[string]interface{}{ + "transactions": []string{TestTransactionID}, + } + payloadBytes, _ := json.Marshal(payload) + fmt.Printf("原始请求体: %s\n", string(payloadBytes)) + + // 2. 加密数据 + if DeviceSecret == "YOUR_DEVICE_SECRET_HERE" { + log.Fatal("❌ 请在代码中设置 DeviceSecret (对应后台配置的 Security Secret)") + } + encryptedData, iv, err := pkgaes.Encrypt(payloadBytes, DeviceSecret) + if err != nil { + log.Fatalf("加密失败: %v", err) + } + + // 3. 构造最终的请求体 (符合 DeviceMiddleware 要求的格式) + // DeviceMiddleware 期望的格式是: { "data": "Base64Cipher", "time": "Nonce/IV" } + // 或者直接在 URL Query 中传 ?data=...&time=... + // 这里我们模拟 POST JSON body 的方式 + finalPayload := map[string]interface{}{ + "data": encryptedData, + "time": iv, + } + finalBytes, _ := json.Marshal(finalPayload) + fmt.Printf("加密后请求体: %s\n", string(finalBytes)) + + url := fmt.Sprintf("%s/v1/public/iap/apple/restore", BaseURL) + req, _ := http.NewRequest("POST", url, strings.NewReader(string(finalBytes))) + req.Header.Set("Content-Type", "application/json") + + // 添加必要的 Header 以通过 DeviceMiddleware + req.Header.Set("Login-Type", "device") // 触发 DeviceMiddleware 的解密逻辑 + // 注意:这里需要替换为真实有效的 Bearer Token,否则会报 401 + // 您可以先登录后台或者使用 cmd/test_apple_iap 工具生成的 token 也是不可用的,必须是业务系统的 token + // 为了演示,这里留空,实际运行前请填入 + if UserToken != "YOUR_USER_TOKEN_HERE" { + req.Header.Set("Authorization", "Bearer "+UserToken) + } else { + fmt.Println("⚠️ 警告: 未设置 UserToken,请求可能会失败 (401 Unauthorized)") + } + + client := &http.Client{Timeout: 10 * time.Second} + start := time.Now() + resp, err := client.Do(req) + if err != nil { + log.Fatalf("请求失败: %v", err) + } + defer resp.Body.Close() + + duration := time.Since(start) + body, _ := io.ReadAll(resp.Body) + + fmt.Printf("请求耗时: %v\n", duration) + fmt.Printf("状态码: %d\n", resp.StatusCode) + fmt.Printf("响应内容: %s\n", string(body)) + + if resp.StatusCode == 200 { + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err == nil { + // 检查业务状态码 + if code, ok := result["code"].(float64); ok && int(code) != 200 { + fmt.Printf("❌ 业务处理失败: code=%d, msg=%s\n", int(code), result["msg"]) + return + } + + fmt.Println("✅ Restore 接口调用成功!") + if data, ok := result["data"].(map[string]interface{}); ok { + if success, ok := data["success"].(bool); ok && success { + fmt.Println(" 业务处理成功: success=true") + } else { + fmt.Println(" 业务处理结果未知:", data) + } + } else { + fmt.Println(" 无数据返回或格式不符") + } + } + } else { + fmt.Println("❌ 接口调用失败") + } +} diff --git a/internal/handler/public/iap/apple/restoreHandler.go b/internal/handler/public/iap/apple/restoreHandler.go index 5b6b775..736cee5 100644 --- a/internal/handler/public/iap/apple/restoreHandler.go +++ b/internal/handler/public/iap/apple/restoreHandler.go @@ -21,4 +21,3 @@ func RestoreAppleTransactionsHandler(svcCtx *svc.ServiceContext) func(c *gin.Con result.HttpResult(c, map[string]bool{"success": err == nil}, err) } } - diff --git a/internal/logic/public/iap/apple/attachTransactionLogic.go b/internal/logic/public/iap/apple/attachTransactionLogic.go index 6d61e5e..608d3af 100644 --- a/internal/logic/public/iap/apple/attachTransactionLogic.go +++ b/internal/logic/public/iap/apple/attachTransactionLogic.go @@ -81,6 +81,19 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest } } exp := iapapple.CalcExpire(txPayload.PurchaseDate, duration) + + if existTx != nil && existTx.Id > 0 { + token := fmt.Sprintf("iap:%s", txPayload.OriginalTransactionId) + existSub, err := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, token) + if err == nil && existSub != nil && existSub.Id > 0 { + // Already processed, return success + return &types.AttachAppleTransactionResponse{ + ExpiresAt: exp.Unix(), + Tier: tier, + }, nil + } + } + sum := sha256.Sum256([]byte(req.SignedTransactionJWS)) jwsHash := hex.EncodeToString(sum[:]) iapTx := &iapmodel.Transaction{