qnc-server-tob/app/user/cmd/api/internal/service/applepayService.go
2025-01-10 00:09:25 +08:00

169 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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