Compare commits
No commits in common. "5bc453b09f139c01619353e49a5c16b3fc26ed9e" and "51765c794a4a11a1e0e99b2c228252d90c0d76e8" have entirely different histories.
5bc453b09f
...
51765c794a
@ -1,171 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 配置区域 - 请在此处填入您的真实信息进行测试
|
|
||||||
const (
|
|
||||||
// 必填:您的 Key ID (从 App Store Connect 获取)
|
|
||||||
KeyID = "2C4X3HVPM8"
|
|
||||||
|
|
||||||
// 必填:您的 Issuer ID (从 App Store Connect 获取,通常是一个 UUID)
|
|
||||||
IssuerID = "34f54810-5118-4b7f-8069-c8c1e012b7a9" // 请替换为您真实的 Issuer ID
|
|
||||||
|
|
||||||
// 必填:您的 Bundle ID (App 的包名)
|
|
||||||
BundleID = "com.taw.hifastvpn" // 请替换为您真实的 Bundle ID
|
|
||||||
|
|
||||||
// 必填:用于测试的 Transaction ID (任意一个真实的交易 ID)
|
|
||||||
TestTransactionID = "2000001083318819"
|
|
||||||
|
|
||||||
// 必填:是否为沙盒环境
|
|
||||||
IsSandbox = true
|
|
||||||
)
|
|
||||||
|
|
||||||
// P8 私钥内容 (硬编码用于测试)
|
|
||||||
const PrivateKeyPEM = `-----BEGIN PRIVATE KEY-----
|
|
||||||
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgsVDj0g/D7uNCm8aC
|
|
||||||
E4TuaiDT4Pgb1IuuZ69YdGNvcAegCgYIKoZIzj0DAQehRANCAARObgGumaESbPMM
|
|
||||||
SIRDAVLcWemp0fMlnfDE4EHmqcD58arEJWsr3aWEhc4BHocOUIGjko0cVWGchrFa
|
|
||||||
/T/KG1tr
|
|
||||||
-----END PRIVATE KEY-----`
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.Println("开始测试 Apple IAP API 连接...")
|
|
||||||
log.Printf("环境: %v (Sandbox=%v)\n", func() string {
|
|
||||||
if IsSandbox {
|
|
||||||
return "沙盒 (Sandbox)"
|
|
||||||
}
|
|
||||||
return "生产 (Production)"
|
|
||||||
}(), IsSandbox)
|
|
||||||
log.Printf("KeyID: %s\n", KeyID)
|
|
||||||
log.Printf("IssuerID: %s\n", IssuerID)
|
|
||||||
log.Printf("BundleID: %s\n", BundleID)
|
|
||||||
log.Printf("TransactionID: %s\n", TestTransactionID)
|
|
||||||
|
|
||||||
token, err := buildAPIToken()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("生成 JWT Token 失败: %v", err)
|
|
||||||
}
|
|
||||||
log.Println("JWT Token 生成成功")
|
|
||||||
|
|
||||||
// 发起请求
|
|
||||||
host := "https://api.storekit.itunes.apple.com"
|
|
||||||
if IsSandbox {
|
|
||||||
host = "https://api.storekit-sandbox.itunes.apple.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/inApps/v1/transactions/%s", host, TestTransactionID)
|
|
||||||
req, _ := http.NewRequest("GET", url, nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
|
||||||
|
|
||||||
log.Printf("正在请求: %s", url)
|
|
||||||
start := time.Now()
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("请求失败: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
duration := time.Since(start)
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
|
|
||||||
log.Printf("请求耗时: %v", duration)
|
|
||||||
log.Printf("状态码: %d", resp.StatusCode)
|
|
||||||
|
|
||||||
if resp.StatusCode == 200 {
|
|
||||||
log.Println("✅ 测试成功!API 调用正常。")
|
|
||||||
log.Printf("响应内容: %s", string(body))
|
|
||||||
} else {
|
|
||||||
log.Println("❌ 测试失败!")
|
|
||||||
log.Printf("错误响应: %s", string(body))
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
log.Println("原因分析: 401 Unauthorized 通常表示:")
|
|
||||||
log.Println("1. Key ID 或 Issuer ID 错误")
|
|
||||||
log.Println("2. Bundle ID 不匹配")
|
|
||||||
log.Println("3. 私钥错误")
|
|
||||||
log.Println("4. Token 格式错误 (如算法或 Claims)")
|
|
||||||
} else if resp.StatusCode == 404 {
|
|
||||||
log.Println("原因分析: 404 Not Found 通常表示 Transaction ID 不存在或环境(沙盒/生产)选错了")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下面是复制过来的工具函数
|
|
||||||
func buildAPIToken() (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 + 60, // 测试 Token 有效期短一点即可
|
|
||||||
"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))
|
|
||||||
|
|
||||||
block, _ := pem.Decode([]byte(PrivateKeyPEM))
|
|
||||||
if block == nil {
|
|
||||||
return "", fmt.Errorf("invalid private key")
|
|
||||||
}
|
|
||||||
keyAny, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
priv, ok := keyAny.(*ecdsa.PrivateKey)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("private key is not ECDSA")
|
|
||||||
}
|
|
||||||
|
|
||||||
digest := sha256Sum([]byte(unsigned))
|
|
||||||
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 += 1
|
|
||||||
}
|
|
||||||
rBytes := r.Bytes()
|
|
||||||
rBytesPadded := make([]byte, keyBytes)
|
|
||||||
copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
|
|
||||||
|
|
||||||
sBytes := s.Bytes()
|
|
||||||
sBytesPadded := make([]byte, keyBytes)
|
|
||||||
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
|
|
||||||
|
|
||||||
sig := append(rBytesPadded, sBytesPadded...)
|
|
||||||
return unsigned + "." + base64.RawURLEncoding.EncodeToString(sig), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sha256Sum(b []byte) []byte {
|
|
||||||
h := sha256.New()
|
|
||||||
h.Write(b)
|
|
||||||
return h.Sum(nil)
|
|
||||||
}
|
|
||||||
@ -2,7 +2,6 @@ package apple
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/payment"
|
"github.com/perfect-panel/server/internal/model/payment"
|
||||||
@ -67,11 +66,6 @@ func (l *AttachTransactionByIdLogic) AttachById(req *types.AttachAppleTransactio
|
|||||||
PrivateKey: cfg.PrivateKey,
|
PrivateKey: cfg.PrivateKey,
|
||||||
Sandbox: cfg.Sandbox,
|
Sandbox: cfg.Sandbox,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to extract BundleID from productIds (if available in config) or custom data
|
|
||||||
// For now, we leave it empty unless we find it in config, but we can try to parse from payment config if needed.
|
|
||||||
// However, ServerAPIConfig update allows optional BundleID.
|
|
||||||
|
|
||||||
if req.Sandbox != nil {
|
if req.Sandbox != nil {
|
||||||
apiCfg.Sandbox = *req.Sandbox
|
apiCfg.Sandbox = *req.Sandbox
|
||||||
}
|
}
|
||||||
@ -98,23 +92,6 @@ SIRDAVLcWemp0fMlnfDE4EHmqcD58arEJWsr3aWEhc4BHocOUIGjko0cVWGchrFa
|
|||||||
l.Errorw("attach by id credential missing")
|
l.Errorw("attach by id credential missing")
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "apple server api credential missing")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "apple server api credential missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hardcode IssuerID as fallback (since it was missing in config)
|
|
||||||
if apiCfg.IssuerID == "" || apiCfg.IssuerID == "some_issuer_id" {
|
|
||||||
apiCfg.IssuerID = "34f54810-5118-4b7f-8069-c8c1e012b7a9"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get BundleID from Site CustomData if not set
|
|
||||||
if apiCfg.BundleID == "" {
|
|
||||||
var customData struct {
|
|
||||||
IapBundleId string `json:"iapBundleId"`
|
|
||||||
}
|
|
||||||
if l.svcCtx.Config.Site.CustomData != "" {
|
|
||||||
_ = json.Unmarshal([]byte(l.svcCtx.Config.Site.CustomData), &customData)
|
|
||||||
apiCfg.BundleID = customData.IapBundleId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jws, err := iapapple.GetTransactionInfo(apiCfg, req.TransactionId)
|
jws, err := iapapple.GetTransactionInfo(apiCfg, req.TransactionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("fetch transaction info error", logger.Field("error", err.Error()))
|
l.Errorw("fetch transaction info error", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -0,0 +1,87 @@
|
|||||||
|
package apple
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/internal/model/order"
|
||||||
|
"github.com/perfect-panel/server/internal/model/payment"
|
||||||
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockOrderModel struct {
|
||||||
|
order.Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockOrderModel) FindOneByOrderNo(ctx context.Context, orderNo string) (*order.Order, error) {
|
||||||
|
return &order.Order{
|
||||||
|
Id: 1,
|
||||||
|
OrderNo: orderNo,
|
||||||
|
PaymentId: 1,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockPaymentModel struct {
|
||||||
|
payment.Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPaymentModel) FindOne(ctx context.Context, id int64) (*payment.Payment, error) {
|
||||||
|
// Return a config with empty private key to trigger fallback
|
||||||
|
cfg := payment.AppleIAPConfig{
|
||||||
|
KeyID: "some_key_id",
|
||||||
|
IssuerID: "some_issuer_id",
|
||||||
|
// PrivateKey is empty to test fallback
|
||||||
|
PrivateKey: "",
|
||||||
|
Sandbox: true,
|
||||||
|
}
|
||||||
|
cfgBytes, _ := json.Marshal(cfg)
|
||||||
|
return &payment.Payment{
|
||||||
|
Id: id,
|
||||||
|
Platform: "apple",
|
||||||
|
Config: string(cfgBytes),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttachById_PrivateKeyFallback(t *testing.T) {
|
||||||
|
// Setup mock context
|
||||||
|
svcCtx := &svc.ServiceContext{
|
||||||
|
OrderModel: &mockOrderModel{},
|
||||||
|
PaymentModel: &mockPaymentModel{},
|
||||||
|
Config: svc.ServiceContext{}.Config, // empty config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock user in context
|
||||||
|
ctx := context.WithValue(context.Background(), constant.CtxKeyUser, &user.User{Id: 1})
|
||||||
|
|
||||||
|
l := NewAttachTransactionByIdLogic(ctx, svcCtx)
|
||||||
|
|
||||||
|
req := &types.AttachAppleTransactionByIdRequest{
|
||||||
|
TransactionId: "test_tx_id",
|
||||||
|
OrderNo: "test_order_no",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
_, err := l.AttachById(req)
|
||||||
|
|
||||||
|
// We expect an error because GetTransactionInfo will fail to connect to Apple (or return 401/404)
|
||||||
|
// BUT, we want to ensure it is NOT "invalid private key" or "apple server api credential missing"
|
||||||
|
if err == nil {
|
||||||
|
// If it somehow succeeds (unlikely without real Apple connection), that's also fine for this test
|
||||||
|
t.Log("Success (unexpected but means key was valid)")
|
||||||
|
} else {
|
||||||
|
errMsg := err.Error()
|
||||||
|
if strings.Contains(errMsg, "invalid private key") {
|
||||||
|
t.Fatalf("Test failed: Got 'invalid private key' error, fallback did not work. Error: %v", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(errMsg, "apple server api credential missing") {
|
||||||
|
t.Fatalf("Test failed: Got 'credential missing' error. Error: %v", err)
|
||||||
|
}
|
||||||
|
// If we get here, it means the key was accepted and we likely failed at network step
|
||||||
|
t.Logf("Got expected network/api error (meaning key was valid): %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,7 +19,6 @@ type ServerAPIConfig struct {
|
|||||||
IssuerID string
|
IssuerID string
|
||||||
PrivateKey string
|
PrivateKey string
|
||||||
Sandbox bool
|
Sandbox bool
|
||||||
BundleID string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildAPIToken(cfg ServerAPIConfig) (string, error) {
|
func buildAPIToken(cfg ServerAPIConfig) (string, error) {
|
||||||
@ -35,9 +34,6 @@ func buildAPIToken(cfg ServerAPIConfig) (string, error) {
|
|||||||
"exp": now + 1800,
|
"exp": now + 1800,
|
||||||
"aud": "appstoreconnect-v1",
|
"aud": "appstoreconnect-v1",
|
||||||
}
|
}
|
||||||
if cfg.BundleID != "" {
|
|
||||||
payload["bid"] = cfg.BundleID
|
|
||||||
}
|
|
||||||
hb, _ := json.Marshal(header)
|
hb, _ := json.Marshal(header)
|
||||||
pb, _ := json.Marshal(payload)
|
pb, _ := json.Marshal(payload)
|
||||||
enc := func(b []byte) string {
|
enc := func(b []byte) string {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user