2024-11-27 01:58:05 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/ecdsa"
|
|
|
|
|
"crypto/x509"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"encoding/pem"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io/ioutil"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"time"
|
2025-04-27 12:17:18 +08:00
|
|
|
|
"tyc-server/app/main/api/internal/config"
|
2025-04-09 15:58:06 +08:00
|
|
|
|
|
|
|
|
|
"github.com/golang-jwt/jwt/v4"
|
2024-11-27 01:58:05 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|