package payment import ( "bytes" "context" "crypto/ecdsa" "crypto/x509" "encoding/json" "encoding/pem" "errors" "fmt" "io/ioutil" "net/http" "strconv" "time" "github.com/golang-jwt/jwt/v4" ) // ApplePayConfig 苹果支付配置 type ApplePayConfig struct { BundleID string `json:"bundleId"` // 应用 Bundle ID KeyID string `json:"keyId"` // 密钥 ID TeamID string `json:"teamId"` // 团队 ID PrivateKey string `json:"privateKey"` // 私钥内容 IsProduction bool `json:"isProduction"` // 是否生产环境 NotifyURL string `json:"notifyUrl"` // 支付结果通知地址 RefundNotifyURL string `json:"refundNotifyUrl"` // 退款结果通知地址 SharedSecret string `json:"sharedSecret"` // 共享密钥 Extra map[string]string `json:"extra"` // 额外配置 } // ApplePayService 苹果支付服务 type ApplePayService struct { config ApplePayConfig } // NewApplePayService 创建苹果支付服务 func NewApplePayService(c ApplePayConfig) (*ApplePayService, error) { if c.BundleID == "" { return nil, errors.New("Bundle ID 不能为空") } if c.KeyID == "" { return nil, errors.New("Key ID 不能为空") } if c.TeamID == "" { return nil, errors.New("Team ID 不能为空") } if c.PrivateKey == "" { return nil, errors.New("私钥不能为空") } return &ApplePayService{ config: c, }, nil } // CreateOrder 创建苹果支付订单 func (a *ApplePayService) CreateOrder(ctx context.Context, req *AppleCreateOrderRequest) (*AppleCreateOrderResponse, error) { if err := a.validateCreateOrderRequest(req); err != nil { return nil, err } // 生成 JWT token token, err := a.generateJWT() if err != nil { return nil, fmt.Errorf("生成 JWT token 失败: %w", err) } return &AppleCreateOrderResponse{ OrderNo: req.OutTradeNo, PayParams: token, ExpireTime: time.Now().Add(30 * time.Minute), // 苹果支付订单默认30分钟过期 CreateTime: time.Now(), }, nil } // VerifyReceipt 验证支付收据 func (a *ApplePayService) VerifyReceipt(ctx context.Context, receipt string) (*QueryOrderResponse, error) { if receipt == "" { return nil, errors.New("收据不能为空") } // 构建验证请求 verifyURL := "https://sandbox.itunes.apple.com/verifyReceipt" if a.config.IsProduction { verifyURL = "https://buy.itunes.apple.com/verifyReceipt" } reqBody := map[string]interface{}{ "receipt-data": receipt, "password": a.config.SharedSecret, } // 发送验证请求 resp, err := a.sendVerifyRequest(ctx, verifyURL, reqBody) if err != nil { return nil, fmt.Errorf("验证收据失败: %w", err) } // 解析响应 if resp.Status != 0 { return nil, fmt.Errorf("验证收据失败: %d", resp.Status) } // 获取最新的交易信息 if len(resp.Receipt.InApp) == 0 { return nil, errors.New("未找到交易信息") } latestReceipt := resp.Receipt.InApp[len(resp.Receipt.InApp)-1] purchaseDate, _ := time.Parse("2006-01-02 15:04:05 -0700", latestReceipt.PurchaseDate) // 转换价格字符串为浮点数 price, err := strconv.ParseFloat(latestReceipt.Price, 64) if err != nil { return nil, fmt.Errorf("解析价格失败: %w", err) } return &QueryOrderResponse{ OrderNo: latestReceipt.TransactionID, TransactionID: latestReceipt.TransactionID, Status: a.convertTransactionState(latestReceipt.TransactionState), Amount: price / 100, // 苹果支付金额单位为分 PayTime: purchaseDate, PayMethod: MethodApple, }, nil } // Refund 申请退款 func (a *ApplePayService) Refund(ctx context.Context, req *RefundRequest) (*RefundResponse, error) { if err := a.validateRefundRequest(req); err != nil { return nil, err } // 苹果支付不支持主动退款,需要通过 App Store Connect 处理 return nil, errors.New("苹果支付不支持主动退款") } // QueryOrder 查询订单 func (a *ApplePayService) QueryOrder(ctx context.Context, req *QueryOrderRequest) (*QueryOrderResponse, error) { if err := a.validateQueryRequest(req); err != nil { return nil, err } // 苹果支付不支持主动查询订单状态,需要通过验证收据获取 return nil, errors.New("苹果支付不支持主动查询订单") } // HandlePaymentNotification 处理支付结果通知 func (a *ApplePayService) HandlePaymentNotification(req *http.Request) (*QueryOrderResponse, error) { // 苹果支付的通知是通过 App Store Server Notifications 发送的 // 需要实现相应的通知处理逻辑 return nil, errors.New("苹果支付通知处理暂未实现") } // validateCreateOrderRequest 验证创建订单请求参数 func (a *ApplePayService) validateCreateOrderRequest(req *AppleCreateOrderRequest) error { if req.Amount <= 0 { return errors.New("支付金额必须大于0") } if req.Subject == "" { return errors.New("商品描述不能为空") } if req.OutTradeNo == "" { return errors.New("商户订单号不能为空") } return nil } // validateRefundRequest 验证退款请求参数 func (a *ApplePayService) validateRefundRequest(req *RefundRequest) error { if req.OrderNo == "" { return errors.New("商户订单号不能为空") } if req.RefundNo == "" { return errors.New("商户退款单号不能为空") } if req.RefundAmount <= 0 { return errors.New("退款金额必须大于0") } if req.RefundAmount > req.TotalAmount { return errors.New("退款金额不能大于订单总金额") } return nil } // validateQueryRequest 验证查询请求参数 func (a *ApplePayService) validateQueryRequest(req *QueryOrderRequest) error { if req.OrderNo == "" && req.TransactionID == "" { return errors.New("商户订单号和交易号不能同时为空") } return nil } // generateJWT 生成 JWT token func (a *ApplePayService) generateJWT() (string, error) { now := time.Now() claims := jwt.MapClaims{ "iss": a.config.TeamID, "iat": now.Unix(), "exp": now.Add(24 * time.Hour).Unix(), "aud": "appstoreconnect-v1", "bid": a.config.BundleID, } token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) token.Header["kid"] = a.config.KeyID privateKey, err := jwt.ParseECPrivateKeyFromPEM([]byte(a.config.PrivateKey)) if err != nil { return "", fmt.Errorf("解析私钥失败: %w", err) } return token.SignedString(privateKey) } // convertTransactionState 转换交易状态 func (a *ApplePayService) convertTransactionState(state string) PaymentStatus { switch state { case "0": // Purchased return StatusSuccess case "1": // Canceled return StatusClosed case "2": // Failed return StatusFailed default: return StatusFailed } } // GenerateOutTradeNo 生成商户订单号 func (a *ApplePayService) GenerateOutTradeNo() string { return GenerateOrderNo(ApplePrefix) } // AppleVerifyResponse 苹果支付验证响应 type AppleVerifyResponse struct { Status int `json:"status"` Receipt Receipt `json:"receipt"` Environment string `json:"environment"` } // Receipt 收据信息 type Receipt struct { BundleID string `json:"bundle_id"` ApplicationVersion string `json:"application_version"` InApp []InApp `json:"in_app"` } // InApp 应用内购买信息 type InApp struct { Quantity string `json:"quantity"` ProductID string `json:"product_id"` TransactionID string `json:"transaction_id"` OriginalTransactionID string `json:"original_transaction_id"` PurchaseDate string `json:"purchase_date"` PurchaseDateMS string `json:"purchase_date_ms"` PurchaseDatePST string `json:"purchase_date_pst"` OriginalPurchaseDate string `json:"original_purchase_date"` OriginalPurchaseDateMS string `json:"original_purchase_date_ms"` OriginalPurchaseDatePST string `json:"original_purchase_date_pst"` ExpiresDate string `json:"expires_date"` ExpiresDateMS string `json:"expires_date_ms"` ExpiresDatePST string `json:"expires_date_pst"` WebOrderLineItemID string `json:"web_order_line_item_id"` IsTrialPeriod string `json:"is_trial_period"` IsInIntroOfferPeriod string `json:"is_in_intro_offer_period"` TransactionState string `json:"transaction_state"` Price string `json:"price"` } func loadPrivateKey(path string) (*ecdsa.PrivateKey, error) { data, err := ioutil.ReadFile(path) if err != nil { return nil, fmt.Errorf("读取私钥文件失败: %w", err) } block, _ := pem.Decode(data) if block == nil || block.Type != "PRIVATE KEY" { return nil, errors.New("无效的私钥数据") } key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("解析私钥失败: %w", err) } ecdsaKey, ok := key.(*ecdsa.PrivateKey) if !ok { return nil, errors.New("私钥类型错误") } 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 } // sendVerifyRequest 发送验证请求 func (a *ApplePayService) sendVerifyRequest(ctx context.Context, url string, body map[string]interface{}) (*AppleVerifyResponse, error) { jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("序列化请求体失败: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonBody)) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("发送请求失败: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("请求失败,状态码: %d", resp.StatusCode) } var verifyResp AppleVerifyResponse if err := json.NewDecoder(resp.Body).Decode(&verifyResp); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } return &verifyResp, nil }