169 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
		
		
			
		
	
	
			169 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
|  | package service | |||
|  | 
 | |||
|  | import ( | |||
|  | 	"context" | |||
|  | 	"crypto/ecdsa" | |||
|  | 	"crypto/x509" | |||
|  | 	"encoding/json" | |||
|  | 	"encoding/pem" | |||
|  | 	"fmt" | |||
|  | 	"github.com/golang-jwt/jwt/v4" | |||
|  | 	"io/ioutil" | |||
|  | 	"net/http" | |||
|  | 	"qnc-server/app/user/cmd/api/internal/config" | |||
|  | 	"strconv" | |||
|  | 	"time" | |||
|  | ) | |||
|  | 
 | |||
|  | // ApplePayService 是 Apple IAP 支付服务的结构体 | |||
|  | type ApplePayService struct { | |||
|  | 	config config.ApplepayConfig // 配置项 | |||
|  | } | |||
|  | 
 | |||
|  | // NewApplePayService 是一个构造函数,用于初始化 ApplePayService | |||
|  | func NewApplePayService(c config.Config) *ApplePayService { | |||
|  | 	return &ApplePayService{ | |||
|  | 		config: c.Applepay, | |||
|  | 	} | |||
|  | } | |||
|  | func (a *ApplePayService) GetIappayAppID(productName string) string { | |||
|  | 	return fmt.Sprintf("%s.%s", a.config.BundleID, productName) | |||
|  | } | |||
|  | 
 | |||
|  | // VerifyReceipt 验证苹果支付凭证 | |||
|  | func (a *ApplePayService) VerifyReceipt(ctx context.Context, receipt string) (*AppleVerifyResponse, error) { | |||
|  | 	var reqUrl string | |||
|  | 	if a.config.Sandbox { | |||
|  | 		reqUrl = a.config.SandboxVerifyURL | |||
|  | 	} else { | |||
|  | 		reqUrl = a.config.ProductionVerifyURL | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 读取私钥 | |||
|  | 	privateKey, err := loadPrivateKey(a.config.LoadPrivateKeyPath) | |||
|  | 	if err != nil { | |||
|  | 		return nil, fmt.Errorf("加载私钥失败:%v", err) | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 生成 JWT | |||
|  | 	token, err := generateJWT(privateKey, a.config.KeyID, a.config.IssuerID) | |||
|  | 	if err != nil { | |||
|  | 		return nil, fmt.Errorf("生成JWT失败:%v", err) | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 构造查询参数 | |||
|  | 	queryParams := fmt.Sprintf("?receipt-data=%s", receipt) | |||
|  | 	fullUrl := reqUrl + queryParams | |||
|  | 
 | |||
|  | 	// 构建 HTTP GET 请求 | |||
|  | 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullUrl, nil) | |||
|  | 	if err != nil { | |||
|  | 		return nil, fmt.Errorf("创建 HTTP 请求失败:%v", err) | |||
|  | 	} | |||
|  | 	req.Header.Set("Content-Type", "application/json") | |||
|  | 	req.Header.Set("Authorization", "Bearer "+token) | |||
|  | 
 | |||
|  | 	// 发送请求 | |||
|  | 	client := &http.Client{Timeout: 10 * time.Second} | |||
|  | 	resp, err := client.Do(req) | |||
|  | 	if err != nil { | |||
|  | 		return nil, fmt.Errorf("请求苹果验证接口失败:%v", err) | |||
|  | 	} | |||
|  | 	defer resp.Body.Close() | |||
|  | 
 | |||
|  | 	// 解析响应 | |||
|  | 	body, err := ioutil.ReadAll(resp.Body) | |||
|  | 	if err != nil { | |||
|  | 		return nil, fmt.Errorf("读取响应体失败:%v", err) | |||
|  | 	} | |||
|  | 
 | |||
|  | 	var verifyResponse AppleVerifyResponse | |||
|  | 	err = json.Unmarshal(body, &verifyResponse) | |||
|  | 	if err != nil { | |||
|  | 		return nil, fmt.Errorf("解析响应体失败:%v", err) | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 根据实际响应处理逻辑 | |||
|  | 	if verifyResponse.Status != 0 { | |||
|  | 		return nil, fmt.Errorf("验证失败,状态码:%d", verifyResponse.Status) | |||
|  | 	} | |||
|  | 
 | |||
|  | 	return &verifyResponse, nil | |||
|  | } | |||
|  | 
 | |||
|  | func loadPrivateKey(path string) (*ecdsa.PrivateKey, error) { | |||
|  | 	data, err := ioutil.ReadFile(path) | |||
|  | 	if err != nil { | |||
|  | 		return nil, err | |||
|  | 	} | |||
|  | 	block, _ := pem.Decode(data) | |||
|  | 	if block == nil || block.Type != "PRIVATE KEY" { | |||
|  | 		return nil, fmt.Errorf("无效的私钥数据") | |||
|  | 	} | |||
|  | 	key, err := x509.ParsePKCS8PrivateKey(block.Bytes) | |||
|  | 	if err != nil { | |||
|  | 		return nil, err | |||
|  | 	} | |||
|  | 	ecdsaKey, ok := key.(*ecdsa.PrivateKey) | |||
|  | 	if !ok { | |||
|  | 		return nil, fmt.Errorf("私钥类型错误") | |||
|  | 	} | |||
|  | 	return ecdsaKey, nil | |||
|  | } | |||
|  | 
 | |||
|  | func generateJWT(privateKey *ecdsa.PrivateKey, keyID, issuerID string) (string, error) { | |||
|  | 	now := time.Now() | |||
|  | 	claims := jwt.RegisteredClaims{ | |||
|  | 		Issuer:    issuerID, | |||
|  | 		IssuedAt:  jwt.NewNumericDate(now), | |||
|  | 		ExpiresAt: jwt.NewNumericDate(now.Add(1 * time.Hour)), | |||
|  | 		Audience:  jwt.ClaimStrings{"appstoreconnect-v1"}, | |||
|  | 	} | |||
|  | 	token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) | |||
|  | 	token.Header["kid"] = keyID | |||
|  | 	tokenString, err := token.SignedString(privateKey) | |||
|  | 	if err != nil { | |||
|  | 		return "", err | |||
|  | 	} | |||
|  | 	return tokenString, nil | |||
|  | } | |||
|  | 
 | |||
|  | // GenerateOutTradeNo 生成唯一订单号 | |||
|  | func (a *ApplePayService) GenerateOutTradeNo() string { | |||
|  | 	length := 16 | |||
|  | 	timestamp := time.Now().UnixNano() | |||
|  | 	timeStr := strconv.FormatInt(timestamp, 10) | |||
|  | 	randomPart := strconv.Itoa(int(timestamp % 1e6)) | |||
|  | 	combined := timeStr + randomPart | |||
|  | 
 | |||
|  | 	if len(combined) >= length { | |||
|  | 		return combined[:length] | |||
|  | 	} | |||
|  | 
 | |||
|  | 	for len(combined) < length { | |||
|  | 		combined += strconv.Itoa(int(timestamp % 10)) | |||
|  | 	} | |||
|  | 
 | |||
|  | 	return combined | |||
|  | } | |||
|  | 
 | |||
|  | // AppleVerifyResponse 定义苹果验证接口的响应结构 | |||
|  | type AppleVerifyResponse struct { | |||
|  | 	Status  int      `json:"status"`  // 验证状态码:0 表示收据有效 | |||
|  | 	Receipt *Receipt `json:"receipt"` // 收据信息 | |||
|  | } | |||
|  | 
 | |||
|  | // Receipt 定义收据的精简结构 | |||
|  | type Receipt struct { | |||
|  | 	BundleID string      `json:"bundle_id"` // 应用的 Bundle ID | |||
|  | 	InApp    []InAppItem `json:"in_app"`    // 应用内购买记录 | |||
|  | } | |||
|  | 
 | |||
|  | // InAppItem 定义单条交易记录 | |||
|  | type InAppItem struct { | |||
|  | 	ProductID       string `json:"product_id"`              // 商品 ID | |||
|  | 	TransactionID   string `json:"transaction_id"`          // 交易 ID | |||
|  | 	PurchaseDate    string `json:"purchase_date"`           // 购买日期 (ISO 8601) | |||
|  | 	OriginalTransID string `json:"original_transaction_id"` // 原始交易 ID | |||
|  | } |