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"
|
||
"strconv"
|
||
"time"
|
||
"tydata-server/app/main/api/internal/config"
|
||
|
||
"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
|
||
}
|