feat(appleIAP): 实现苹果应用内购买通知处理逻辑
Build docker and publish / build (20.15.1) (push) Successful in 6m36s
Build docker and publish / build (20.15.1) (push) Successful in 6m36s
添加苹果IAP通知处理功能,包括解析和验证JWS签名、处理交易状态变更 新增订单号字段用于关联订单处理 实现交易记录的创建和更新逻辑 处理订阅状态的变更和过期时间计算
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
iapmodel "github.com/perfect-panel/server/internal/model/iap/apple"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
iapapple "github.com/perfect-panel/server/pkg/iap/apple"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AppleIAPNotifyLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewAppleIAPNotifyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AppleIAPNotifyLogic {
|
||||
return &AppleIAPNotifyLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error {
|
||||
txPayload, _, err := iapapple.VerifyNotificationSignedPayload(signedPayload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
|
||||
var existing *iapmodel.Transaction
|
||||
existing, _ = iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txPayload.OriginalTransactionId)
|
||||
if existing == nil || existing.Id == 0 {
|
||||
rec := &iapmodel.Transaction{
|
||||
UserId: 0,
|
||||
OriginalTransactionId: txPayload.OriginalTransactionId,
|
||||
TransactionId: txPayload.TransactionId,
|
||||
ProductId: txPayload.ProductId,
|
||||
PurchaseAt: txPayload.PurchaseDate,
|
||||
RevocationAt: txPayload.RevocationDate,
|
||||
JWSHash: "",
|
||||
}
|
||||
if e := db.Model(&iapmodel.Transaction{}).Create(rec).Error; e != nil {
|
||||
return e
|
||||
}
|
||||
} else {
|
||||
if txPayload.RevocationDate != nil {
|
||||
if e := db.Model(&iapmodel.Transaction{}).
|
||||
Where("original_transaction_id = ?", txPayload.OriginalTransactionId).
|
||||
Update("revocation_at", txPayload.RevocationDate).Error; e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
pm, _ := iapapple.ParseProductMap(l.svcCtx.Config.Site.CustomData)
|
||||
m := pm.Items[txPayload.ProductId]
|
||||
token := "iap:" + txPayload.OriginalTransactionId
|
||||
sub, e := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, token)
|
||||
if e == nil && sub != nil && sub.Id != 0 {
|
||||
if txPayload.RevocationDate != nil {
|
||||
sub.Status = 3
|
||||
t := *txPayload.RevocationDate
|
||||
sub.FinishedAt = &t
|
||||
sub.ExpireTime = t
|
||||
} else if m.DurationDays > 0 {
|
||||
exp := iapapple.CalcExpire(txPayload.PurchaseDate, m.DurationDays)
|
||||
sub.ExpireTime = exp
|
||||
sub.Status = 1
|
||||
}
|
||||
if e := l.svcCtx.UserModel.UpdateSubscribe(l.ctx, sub, db); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -4,9 +4,12 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hibiken/asynq"
|
||||
iapmodel "github.com/perfect-panel/server/internal/model/iap/apple"
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
@@ -15,9 +18,9 @@ import (
|
||||
iapapple "github.com/perfect-panel/server/pkg/iap/apple"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
queueType "github.com/perfect-panel/server/queue/types"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AttachTransactionLogic struct {
|
||||
@@ -39,10 +42,13 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
|
||||
if !ok || u == nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "invalid access")
|
||||
}
|
||||
txPayload, err := iapapple.ParseTransactionJWS(req.SignedTransactionJWS)
|
||||
txPayload, err := iapapple.VerifyTransactionJWS(req.SignedTransactionJWS)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "invalid jws")
|
||||
}
|
||||
// idempotency: check existing transaction by original id
|
||||
var existTx *iapmodel.Transaction
|
||||
existTx, _ = iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txPayload.OriginalTransactionId)
|
||||
pm, _ := iapapple.ParseProductMap(l.svcCtx.Config.Site.CustomData)
|
||||
m, ok := pm.Items[txPayload.ProductId]
|
||||
var duration int64
|
||||
@@ -73,8 +79,10 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
|
||||
JWSHash: jwsHash,
|
||||
}
|
||||
err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if e := tx.Model(&iapmodel.Transaction{}).Create(iapTx).Error; e != nil {
|
||||
return e
|
||||
if existTx == nil || existTx.Id == 0 {
|
||||
if e := tx.Model(&iapmodel.Transaction{}).Create(iapTx).Error; e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
// insert user_subscribe
|
||||
userSub := user.Subscribe{
|
||||
@@ -89,7 +97,31 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
|
||||
UUID: uuid.New().String(),
|
||||
Status: 1,
|
||||
}
|
||||
return l.svcCtx.UserModel.InsertSubscribe(l.ctx, &userSub, tx)
|
||||
if e := l.svcCtx.UserModel.InsertSubscribe(l.ctx, &userSub, tx); e != nil {
|
||||
return e
|
||||
}
|
||||
// optional: mark related order as paid and enqueue activation
|
||||
if req.OrderNo != "" {
|
||||
orderInfo, e := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
|
||||
if e != nil {
|
||||
// do not fail transaction if order not found; just continue
|
||||
return nil
|
||||
}
|
||||
if orderInfo.Status == 1 {
|
||||
if e := l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, req.OrderNo, 2, tx); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
// enqueue activation regardless (idempotent handler downstream)
|
||||
payload := queueType.ForthwithActivateOrderPayload{OrderNo: req.OrderNo}
|
||||
bytes, _ := json.Marshal(payload)
|
||||
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
|
||||
if _, e := l.svcCtx.Queue.EnqueueContext(l.ctx, task); e != nil {
|
||||
// non-fatal
|
||||
l.Errorw("enqueue activate task error", logger.Field("error", e.Error()))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert error: %v", err.Error())
|
||||
|
||||
Reference in New Issue
Block a user