170 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			170 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package service
 | ||
| 
 | ||
| import (
 | ||
| 	"context"
 | ||
| 	"crypto/ecdsa"
 | ||
| 	"crypto/x509"
 | ||
| 	"encoding/json"
 | ||
| 	"encoding/pem"
 | ||
| 	"fmt"
 | ||
| 	"io/ioutil"
 | ||
| 	"net/http"
 | ||
| 	"qnc-server/app/user/cmd/api/internal/config"
 | ||
| 	"strconv"
 | ||
| 	"time"
 | ||
| 
 | ||
| 	"github.com/golang-jwt/jwt/v4"
 | ||
| )
 | ||
| 
 | ||
| // 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
 | ||
| }
 |