Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
实现Apple应用内购支付功能,包括: 1. 新增AppleIAP和ApplePay支付平台枚举 2. 添加IAP验证接口/v1/public/iap/verify处理初购验证 3. 实现Apple服务器通知处理逻辑/v1/iap/notifications 4. 新增JWS验签和JWKS公钥缓存功能 5. 复用现有订单系统处理IAP支付订单 相关文档已更新,包含接入方案和实现细节
135 lines
3.9 KiB
Go
135 lines
3.9 KiB
Go
package notify
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/hibiken/asynq"
|
|
"github.com/perfect-panel/server/internal/model/order"
|
|
"github.com/perfect-panel/server/internal/svc"
|
|
"github.com/perfect-panel/server/pkg/appleiap"
|
|
"github.com/perfect-panel/server/pkg/logger"
|
|
"github.com/perfect-panel/server/pkg/payment"
|
|
queueType "github.com/perfect-panel/server/queue/types"
|
|
)
|
|
|
|
// AppleIAPNotifyLogic 处理 Apple Server Notifications v2 的逻辑
|
|
// 功能: 验签与事件解析(此处提供最小骨架),将续期/初购事件转换为订单并入队赋权
|
|
// 参数: HTTP 请求
|
|
// 返回: 错误信息
|
|
type AppleIAPNotifyLogic struct {
|
|
logger.Logger
|
|
ctx context.Context
|
|
svcCtx *svc.ServiceContext
|
|
}
|
|
|
|
// NewAppleIAPNotifyLogic 创建逻辑实例
|
|
// 参数: 上下文, 服务上下文
|
|
// 返回: 逻辑指针
|
|
func NewAppleIAPNotifyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AppleIAPNotifyLogic {
|
|
return &AppleIAPNotifyLogic{Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
|
}
|
|
|
|
// AppleNotification 简化的通知结构(骨架)
|
|
type rawPayload struct {
|
|
SignedPayload string `json:"signedPayload"`
|
|
}
|
|
|
|
type transactionInfo struct {
|
|
OriginalTransactionId string `json:"originalTransactionId"`
|
|
TransactionId string `json:"transactionId"`
|
|
ProductId string `json:"productId"`
|
|
}
|
|
|
|
// Handle 处理通知
|
|
// 参数: *http.Request
|
|
// 返回: error
|
|
func (l *AppleIAPNotifyLogic) Handle(r *http.Request) error {
|
|
body, _ := io.ReadAll(r.Body)
|
|
var rp rawPayload
|
|
if err := json.Unmarshal(body, &rp); err != nil {
|
|
l.Errorw("[AppleIAP] Unmarshal request failed", logger.Field("error", err.Error()))
|
|
return err
|
|
}
|
|
claims, env, err := appleiap.VerifyAutoEnv(rp.SignedPayload)
|
|
if err != nil {
|
|
l.Errorw("[AppleIAP] Verify payload failed", logger.Field("error", err.Error()))
|
|
return err
|
|
}
|
|
t, _ := claims["notificationType"].(string)
|
|
data, _ := claims["data"].(map[string]interface{})
|
|
sti, _ := data["signedTransactionInfo"].(string)
|
|
txClaims, err := appleiap.VerifyWithEnv(env, sti)
|
|
if err != nil {
|
|
l.Errorw("[AppleIAP] Verify transaction failed", logger.Field("error", err.Error()))
|
|
return err
|
|
}
|
|
b, _ := json.Marshal(txClaims)
|
|
var tx transactionInfo
|
|
_ = json.Unmarshal(b, &tx)
|
|
|
|
switch t {
|
|
case "INITIAL_BUY":
|
|
return l.processInitialBuy(env, tx)
|
|
case "DID_RENEW":
|
|
return l.processRenew(env, tx)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// createPaidOrderAndEnqueue 创建已支付订单并入队赋权/续费
|
|
// 参数: AppleNotification, 订单类型
|
|
// 返回: error
|
|
func (l *AppleIAPNotifyLogic) processInitialBuy(env string, tx transactionInfo) error {
|
|
if tx.OriginalTransactionId == "" || tx.TransactionId == "" {
|
|
return nil
|
|
}
|
|
// if order already exists, ignore
|
|
if oi, err := l.svcCtx.OrderModel.FindOneByTradeNo(l.ctx, tx.OriginalTransactionId); err == nil && oi != nil {
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l *AppleIAPNotifyLogic) processRenew(env string, tx transactionInfo) error {
|
|
if tx.OriginalTransactionId == "" || tx.TransactionId == "" {
|
|
return nil
|
|
}
|
|
oi, err := l.svcCtx.OrderModel.FindOneByTradeNo(l.ctx, tx.OriginalTransactionId)
|
|
if err != nil || oi == nil {
|
|
return nil
|
|
}
|
|
o := &order.Order{
|
|
UserId: oi.UserId,
|
|
OrderNo: tx.TransactionId,
|
|
Type: 2,
|
|
Quantity: 1,
|
|
Price: 0,
|
|
Amount: 0,
|
|
Discount: 0,
|
|
Coupon: "",
|
|
CouponDiscount: 0,
|
|
PaymentId: 0,
|
|
Method: payment.AppleIAP.String(),
|
|
FeeAmount: 0,
|
|
Status: 2,
|
|
IsNew: false,
|
|
SubscribeId: oi.SubscribeId,
|
|
TradeNo: tx.OriginalTransactionId,
|
|
SubscribeToken: oi.SubscribeToken,
|
|
}
|
|
if err := l.svcCtx.OrderModel.Insert(l.ctx, o); err != nil {
|
|
return err
|
|
}
|
|
payload := queueType.ForthwithActivateOrderPayload{OrderNo: o.OrderNo}
|
|
bytes, _ := json.Marshal(payload)
|
|
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
|
|
if _, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|