temp
This commit is contained in:
341
pkg/core/payment/applepayService.go
Normal file
341
pkg/core/payment/applepayService.go
Normal file
@@ -0,0 +1,341 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user