From 1e14e2dbd949ee185d2b110afed324708949d051 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Sat, 7 Mar 2026 07:16:01 -0800 Subject: [PATCH] =?UTF-8?q?map=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iap/apple/attachTransactionLogic.go | 272 ++++++++++++++++-- internal/types/types.go | 8 +- pkg/iap/apple/jws.go | 3 + pkg/iap/apple/types.go | 16 +- queue/logic/order/activateOrderLogic.go | 22 +- 5 files changed, 286 insertions(+), 35 deletions(-) diff --git a/internal/logic/public/iap/apple/attachTransactionLogic.go b/internal/logic/public/iap/apple/attachTransactionLogic.go index 861365a..26366ad 100644 --- a/internal/logic/public/iap/apple/attachTransactionLogic.go +++ b/internal/logic/public/iap/apple/attachTransactionLogic.go @@ -6,10 +6,12 @@ import ( "encoding/hex" "encoding/json" "fmt" + "os" "strconv" "strings" "time" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/google/uuid" "github.com/hibiken/asynq" commonLogic "github.com/perfect-panel/server/internal/logic/common" @@ -35,9 +37,10 @@ type AttachTransactionLogic struct { } const ( - orderTypeSubscribe uint8 = 1 - orderStatusPending uint8 = 1 - orderStatusPaid uint8 = 2 + orderTypeSubscribe uint8 = 1 + orderStatusPending uint8 = 1 + orderStatusPaid uint8 = 2 + orderStatusFinished uint8 = 5 ) func NewAttachTransactionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AttachTransactionLogic { @@ -88,6 +91,17 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "invalid jws") } l.Infow("JWS 验签成功", logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("purchaseAt", txPayload.PurchaseDate)) + tradeNoCandidates := l.getAppleTradeNoCandidates(txPayload) + if err = l.validateOrderTradeNoBinding(orderInfo, tradeNoCandidates); err != nil { + l.Errorw("Apple 交易重复绑定,拒绝处理", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNoCandidates", tradeNoCandidates), logger.Field("error", err.Error())) + l.sendIAPAttachTraceToTelegram("REJECT_DUPLICATE_TRANSACTION", orderInfo, u.Id, orderInfo.SubscribeId, "", orderInfo.Quantity, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "apple transaction already used") + } + tradeNo := "" + if len(tradeNoCandidates) > 0 { + tradeNo = tradeNoCandidates[0] + } + orderAlreadyBound := containsString(tradeNoCandidates, orderInfo.TradeNo) // 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) @@ -203,6 +217,11 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest } } exp := iapapple.CalcExpire(txPayload.PurchaseDate, duration) + shouldAccumulate := duration > 0 && (strings.EqualFold(parsedUnit, "Day") || strings.EqualFold(txPayload.Type, "Consumable")) + accumulateDuration := int64(0) + if shouldAccumulate { + accumulateDuration = duration + } l.Infow("计算订阅到期时间", logger.Field("expireAt", exp), logger.Field("expireUnix", exp.Unix())) var orderLinkedSub *user.Subscribe if !isNewPurchaseOrder && orderInfo != nil && orderInfo.SubscribeToken != "" { @@ -239,60 +258,106 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest } } + existSub, existSubErr := l.findIAPSubscribeByOriginalTransactionID(txPayload.OriginalTransactionId) + if existSubErr != nil && !errors.Is(existSubErr, gorm.ErrRecordNotFound) { + l.Errorw("查询 IAP 订阅失败", logger.Field("error", existSubErr.Error()), logger.Field("originalTransactionId", txPayload.OriginalTransactionId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find iap subscribe error: %v", existSubErr.Error()) + } + if existTx != nil && existTx.Id > 0 { + if orderAlreadyBound { + expiresAt := exp.Unix() + if existSub != nil && existSub.Id > 0 { + expiresAt = existSub.ExpireTime.Unix() + } + if orderLinkedSub != nil && orderLinkedSub.Id > 0 { + expiresAt = orderLinkedSub.ExpireTime.Unix() + } + if singleModeAnchorSub != nil && singleModeAnchorSub.Id > 0 { + expiresAt = singleModeAnchorSub.ExpireTime.Unix() + } + if bindErr := l.bindOrderTradeNo(orderInfo, tradeNo); bindErr != nil { + l.Errorw("回填订单交易号失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNo", tradeNo), logger.Field("error", bindErr.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "bind order trade_no failed: %v", bindErr.Error()) + } + if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, 0); syncErr != nil { + l.Errorw("同订单幂等同步失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error()) + } + l.sendIAPAttachTraceToTelegram("IDEMPOTENT_SAME_ORDER", orderInfo, u.Id, subscribeId, tier, duration, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "") + l.Infow("事务已处理,同订单幂等返回", logger.Field("orderNo", req.OrderNo), logger.Field("expiresAt", expiresAt)) + return &types.AttachAppleTransactionResponse{ExpiresAt: expiresAt, Tier: tier}, nil + } + if isNewPurchaseOrder { + if bindErr := l.bindOrderTradeNo(orderInfo, tradeNo); bindErr != nil { + l.Errorw("写入订单交易号失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNo", tradeNo), logger.Field("error", bindErr.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "bind order trade_no failed: %v", bindErr.Error()) + } if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, 0); syncErr != nil { l.Errorw("事务已处理但同步订单状态失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error()) } + l.sendIAPAttachTraceToTelegram("SUCCESS_NEW_PURCHASE_QUEUE", orderInfo, u.Id, subscribeId, tier, duration, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "") l.Infow("事务已处理,首购订单等待激活队列发放订阅", logger.Field("orderNo", req.OrderNo), logger.Field("expiresAt", exp.Unix())) return &types.AttachAppleTransactionResponse{ExpiresAt: exp.Unix(), Tier: tier}, nil } - existSub, err := l.findIAPSubscribeByOriginalTransactionID(txPayload.OriginalTransactionId) switch { - case err == nil && existSub != nil && existSub.Id > 0: - newExpire, updateErr := l.extendSubscribeForIAP(existSub, exp, subscribeId) + case existSubErr == nil && existSub != nil && existSub.Id > 0: + newExpire, updateErr := l.extendSubscribeForIAP(existSub, exp, accumulateDuration, subscribeId) if updateErr != nil { l.Errorw("刷新 IAP 订阅失败", logger.Field("error", updateErr.Error()), logger.Field("subscribeId", existSub.Id)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update iap subscribe failed: %v", updateErr.Error()) } + if bindErr := l.bindOrderTradeNo(orderInfo, tradeNo); bindErr != nil { + l.Errorw("写入订单交易号失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNo", tradeNo), logger.Field("error", bindErr.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "bind order trade_no failed: %v", bindErr.Error()) + } if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, newExpire.Unix()); syncErr != nil { l.Errorw("同步订单状态失败(existSub)", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error()) } + l.sendIAPAttachTraceToTelegram("SUCCESS_RENEW_EXIST_SUB", orderInfo, u.Id, subscribeId, tier, duration, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "") l.Infow("事务已处理,刷新订阅到期时间", logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("tier", tier), logger.Field("expiresAt", newExpire.Unix())) return &types.AttachAppleTransactionResponse{ ExpiresAt: newExpire.Unix(), Tier: tier, }, nil - case err != nil && !errors.Is(err, gorm.ErrRecordNotFound): - l.Errorw("查询 IAP 订阅失败", logger.Field("error", err.Error()), logger.Field("originalTransactionId", txPayload.OriginalTransactionId)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find iap subscribe error: %v", err.Error()) } if orderLinkedSub != nil { - newExpire, updateErr := l.extendSubscribeForIAP(orderLinkedSub, exp, subscribeId) + newExpire, updateErr := l.extendSubscribeForIAP(orderLinkedSub, exp, accumulateDuration, subscribeId) if updateErr != nil { l.Errorw("刷新订单关联订阅失败", logger.Field("error", updateErr.Error()), logger.Field("subscribeId", orderLinkedSub.Id)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update order subscribe failed: %v", updateErr.Error()) } + if bindErr := l.bindOrderTradeNo(orderInfo, tradeNo); bindErr != nil { + l.Errorw("写入订单交易号失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNo", tradeNo), logger.Field("error", bindErr.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "bind order trade_no failed: %v", bindErr.Error()) + } if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, newExpire.Unix()); syncErr != nil { l.Errorw("同步订单状态失败(orderLinkedSub)", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error()) } + l.sendIAPAttachTraceToTelegram("SUCCESS_RENEW_ORDER_LINKED_SUB", orderInfo, u.Id, subscribeId, tier, duration, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "") l.Infow("事务已处理,刷新订单关联订阅到期时间", logger.Field("orderNo", req.OrderNo), logger.Field("userSubscribeId", orderLinkedSub.Id), logger.Field("expiresAt", newExpire.Unix())) return &types.AttachAppleTransactionResponse{ExpiresAt: newExpire.Unix(), Tier: tier}, nil } if singleModeAnchorSub != nil { - newExpire, updateErr := l.extendSubscribeForIAP(singleModeAnchorSub, exp, subscribeId) + newExpire, updateErr := l.extendSubscribeForIAP(singleModeAnchorSub, exp, accumulateDuration, subscribeId) if updateErr != nil { l.Errorw("刷新单订阅锚点订阅失败", logger.Field("error", updateErr.Error()), logger.Field("subscribeId", singleModeAnchorSub.Id)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update single mode anchor subscribe failed: %v", updateErr.Error()) } + if bindErr := l.bindOrderTradeNo(orderInfo, tradeNo); bindErr != nil { + l.Errorw("写入订单交易号失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNo", tradeNo), logger.Field("error", bindErr.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "bind order trade_no failed: %v", bindErr.Error()) + } if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, newExpire.Unix()); syncErr != nil { l.Errorw("同步订单状态失败(singleModeAnchorSub)", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error()) } + l.sendIAPAttachTraceToTelegram("SUCCESS_RENEW_SINGLE_MODE_ANCHOR", orderInfo, u.Id, subscribeId, tier, duration, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "") l.Infow("事务已处理,刷新单订阅锚点到期时间", logger.Field("userSubscribeId", singleModeAnchorSub.Id), logger.Field("expiresAt", newExpire.Unix())) return &types.AttachAppleTransactionResponse{ExpiresAt: newExpire.Unix(), Tier: tier}, nil } @@ -349,6 +414,10 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest } else { l.Infow("首购订单跳过 attach 阶段订阅写入", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("orderType", orderInfo.Type)) } + if e := l.bindOrderTradeNo(orderInfo, tradeNo, tx); e != nil { + l.Errorw("写入订单交易号失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNo", tradeNo), logger.Field("error", e.Error())) + return e + } if e := l.syncOrderStatusAndEnqueue(orderInfo, exp.Unix(), tx); e != nil { l.Errorw("同步订单状态失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", e.Error())) return e @@ -359,6 +428,7 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest l.Errorw("绑定事务提交失败", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert error: %v", err.Error()) } + l.sendIAPAttachTraceToTelegram("SUCCESS_COMMIT", orderInfo, u.Id, subscribeId, tier, duration, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "") l.Infow("绑定完成", logger.Field("userId", u.Id), logger.Field("tier", tier), logger.Field("expiresAt", exp.Unix())) return &types.AttachAppleTransactionResponse{ ExpiresAt: exp.Unix(), @@ -407,14 +477,11 @@ func (l *AttachTransactionLogic) findIAPSubscribeByOriginalTransactionID(origina return nil, gorm.ErrRecordNotFound } -func (l *AttachTransactionLogic) extendSubscribeForIAP(userSub *user.Subscribe, exp time.Time, subscribeId int64, tx ...*gorm.DB) (time.Time, error) { +func (l *AttachTransactionLogic) extendSubscribeForIAP(userSub *user.Subscribe, exp time.Time, durationDays int64, subscribeId int64, tx ...*gorm.DB) (time.Time, error) { if userSub == nil { return time.Time{}, errors.New("user subscribe is nil") } - newExpire := userSub.ExpireTime - if exp.After(newExpire) { - newExpire = exp - } + newExpire := l.calcIAPRenewalExpire(userSub.ExpireTime, exp, durationDays) userSub.ExpireTime = newExpire if subscribeId > 0 && userSub.SubscribeId != subscribeId { userSub.SubscribeId = subscribeId @@ -426,3 +493,176 @@ func (l *AttachTransactionLogic) extendSubscribeForIAP(userSub *user.Subscribe, } return newExpire, nil } + +func (l *AttachTransactionLogic) calcIAPRenewalExpire(currentExpire time.Time, fallbackExpire time.Time, durationDays int64) time.Time { + if durationDays > 0 { + base := currentExpire + now := time.Now() + if base.Before(now) { + base = now + } + return base.Add(time.Duration(durationDays) * 24 * time.Hour) + } + if fallbackExpire.After(currentExpire) { + return fallbackExpire + } + return currentExpire +} + +func (l *AttachTransactionLogic) getAppleTradeNoCandidates(txPayload *iapapple.TransactionPayload) []string { + if txPayload == nil { + return nil + } + candidates := make([]string, 0, 2) + if txPayload.OriginalTransactionId != "" { + candidates = append(candidates, txPayload.OriginalTransactionId) + } + if txPayload.TransactionId != "" && txPayload.TransactionId != txPayload.OriginalTransactionId { + candidates = append(candidates, txPayload.TransactionId) + } + return candidates +} + +func (l *AttachTransactionLogic) validateOrderTradeNoBinding(orderInfo *ordermodel.Order, tradeNoCandidates []string) error { + if orderInfo == nil || len(tradeNoCandidates) == 0 { + return nil + } + if orderInfo.TradeNo != "" && !containsString(tradeNoCandidates, orderInfo.TradeNo) { + return errors.New("order already bound to another apple transaction") + } + + var boundOrder ordermodel.Order + err := l.svcCtx.DB.WithContext(l.ctx). + Model(&ordermodel.Order{}). + Where("trade_no IN ? AND order_no <> ? AND status IN ?", tradeNoCandidates, orderInfo.OrderNo, []uint8{orderStatusPaid, orderStatusFinished}). + Order("id DESC"). + First(&boundOrder).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + if err != nil { + return err + } + return errors.Errorf("apple transaction already used by order %s", boundOrder.OrderNo) +} + +func (l *AttachTransactionLogic) bindOrderTradeNo(orderInfo *ordermodel.Order, tradeNo string, tx ...*gorm.DB) error { + if orderInfo == nil || tradeNo == "" { + return nil + } + if orderInfo.TradeNo == tradeNo { + return nil + } + if orderInfo.TradeNo != "" && orderInfo.TradeNo != tradeNo { + return errors.New("order already bound to another apple transaction") + } + + orderInfo.TradeNo = tradeNo + if err := l.svcCtx.OrderModel.Update(l.ctx, orderInfo, tx...); err != nil { + return err + } + return nil +} + +func containsString(candidates []string, target string) bool { + if target == "" { + return false + } + for _, item := range candidates { + if item == target { + return true + } + } + return false +} + +func (l *AttachTransactionLogic) sendIAPAttachTraceToTelegram(status string, orderInfo *ordermodel.Order, userID int64, subscribeID int64, subscribeName string, quantity int64, purchaseAt time.Time, transactionID string, originalTransactionID string, note string) { + if l.svcCtx == nil { + return + } + orderNo := "" + if orderInfo != nil { + orderNo = orderInfo.OrderNo + } + if subscribeName == "" { + subscribeName = "-" + } + message := fmt.Sprintf( + "IAP Attach Log [%s]\n订单号: %s\n购买时间: %s\n购买人ID: %d\n订阅信息: %s (subscribe_id=%d, quantity=%d)\ntransaction: %s\noriginal_transaction: %s", + status, + orderNo, + purchaseAt.Format("2006-01-02 15:04:05"), + userID, + subscribeName, + subscribeID, + quantity, + transactionID, + originalTransactionID, + ) + if note != "" { + message += "\n备注: " + note + } + + overrideBotToken := strings.TrimSpace(os.Getenv("TG_BOT_TOKEN")) + overrideChatID := strings.TrimSpace(os.Getenv("TG_CHAT_ID")) + if overrideBotToken != "" && overrideChatID != "" { + if chatID, err := strconv.ParseInt(overrideChatID, 10, 64); err == nil && chatID != 0 { + bot := l.svcCtx.TelegramBot + if bot == nil || strings.TrimSpace(l.svcCtx.Config.Telegram.BotToken) != overrideBotToken { + overrideBot, newErr := tgbotapi.NewBotAPI(overrideBotToken) + if newErr == nil { + bot = overrideBot + } else { + l.Errorw("初始化 TG 覆盖 Bot 失败", logger.Field("error", newErr.Error())) + } + } + if bot != nil { + msg := tgbotapi.NewMessage(chatID, message) + if _, sendErr := bot.Send(msg); sendErr != nil { + l.Errorw("发送 IAP TG 覆盖通道消息失败", logger.Field("error", sendErr.Error())) + } + return + } + } + } + + if l.svcCtx.TelegramBot == nil || !l.svcCtx.Config.Telegram.EnableNotify { + return + } + + if groupChatID, err := strconv.ParseInt(strings.TrimSpace(l.svcCtx.Config.Telegram.GroupChatID), 10, 64); err == nil && groupChatID != 0 { + msg := tgbotapi.NewMessage(groupChatID, message) + if _, sendErr := l.svcCtx.TelegramBot.Send(msg); sendErr != nil { + l.Errorw("发送 IAP TG 群消息失败", logger.Field("error", sendErr.Error())) + } + return + } + + admins, err := l.svcCtx.UserModel.QueryAdminUsers(l.ctx) + if err != nil { + l.Errorw("查询管理员失败(IAP TG日志)", logger.Field("error", err.Error())) + return + } + for _, admin := range admins { + if telegramID, ok := findTelegramAuth(admin); ok { + msg := tgbotapi.NewMessage(telegramID, message) + if _, sendErr := l.svcCtx.TelegramBot.Send(msg); sendErr != nil { + l.Errorw("发送 IAP TG 管理员消息失败", logger.Field("error", sendErr.Error())) + } + } + } +} + +func findTelegramAuth(u *user.User) (int64, bool) { + if u == nil { + return 0, false + } + for _, item := range u.AuthMethods { + if item.AuthType == "telegram" { + if telegramID, err := strconv.ParseInt(item.AuthIdentifier, 10, 64); err == nil { + return telegramID, true + } + } + } + return 0, false +} diff --git a/internal/types/types.go b/internal/types/types.go index 9903e04..e6bb113 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1610,8 +1610,8 @@ type Order struct { TradeNo string `json:"trade_no"` Status uint8 `json:"status"` SubscribeId int64 `json:"subscribe_id"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + CreatedAt int64 `json:"created_at"` // Unix milliseconds + UpdatedAt int64 `json:"updated_at"` // Unix milliseconds } type OrderDetail struct { @@ -1634,8 +1634,8 @@ type OrderDetail struct { Status uint8 `json:"status"` SubscribeId int64 `json:"subscribe_id"` Subscribe Subscribe `json:"subscribe"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + CreatedAt int64 `json:"created_at"` // Unix milliseconds + UpdatedAt int64 `json:"updated_at"` // Unix milliseconds } type OrdersStatistics struct { diff --git a/pkg/iap/apple/jws.go b/pkg/iap/apple/jws.go index 0c024e6..65d4c6b 100644 --- a/pkg/iap/apple/jws.go +++ b/pkg/iap/apple/jws.go @@ -56,6 +56,9 @@ func ParseTransactionJWS(jws string) (*TransactionPayload, error) { if v, ok := raw["productId"].(string); ok { resp.ProductId = v } + if v, ok := raw["type"].(string); ok { + resp.Type = v + } if v, ok := raw["transactionId"].(string); ok { resp.TransactionId = v } diff --git a/pkg/iap/apple/types.go b/pkg/iap/apple/types.go index 05dd61d..10a79aa 100644 --- a/pkg/iap/apple/types.go +++ b/pkg/iap/apple/types.go @@ -3,12 +3,12 @@ package apple import "time" type TransactionPayload struct { - BundleId string `json:"bundleId"` - ProductId string `json:"productId"` - TransactionId string `json:"transactionId"` - OriginalTransactionId string `json:"originalTransactionId"` - PurchaseDate time.Time `json:"purchaseDate"` - RevocationDate *time.Time`json:"revocationDate"` - AppAccountToken string `json:"appAccountToken"` + BundleId string `json:"bundleId"` + ProductId string `json:"productId"` + Type string `json:"type"` + TransactionId string `json:"transactionId"` + OriginalTransactionId string `json:"originalTransactionId"` + PurchaseDate time.Time `json:"purchaseDate"` + RevocationDate *time.Time `json:"revocationDate"` + AppAccountToken string `json:"appAccountToken"` } - diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go index 36cafd7..f94f567 100644 --- a/queue/logic/order/activateOrderLogic.go +++ b/queue/logic/order/activateOrderLogic.go @@ -733,9 +733,8 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order } if iapExpireAt > 0 { - // Apple IAP 续费:attachTransactionLogic 已通过 payload 传入 Apple 端计算的到期时间, - // 直接使用该时间,避免在现有 expire_time 基础上再叠加天数导致双重计算。 - if err = l.updateSubscriptionWithIAPExpire(ctx, userSub, sub, iapExpireAt); err != nil { + // Apple IAP 续费:按“累计加时”语义处理,避免连续购买时仅覆盖到期时间。 + if err = l.updateSubscriptionWithIAPExpire(ctx, userSub, sub, orderInfo, iapExpireAt); err != nil { return err } } else { @@ -776,11 +775,20 @@ func (l *ActivateOrderLogic) getUserSubscription(ctx context.Context, token stri return userSub, nil } -// updateSubscriptionWithIAPExpire 用于 Apple IAP 续费:直接将 Apple 服务端计算的 -// 到期时间写入订阅,同时处理流量重置和 FinishedAt 清零,不再叠加天数。 -func (l *ActivateOrderLogic) updateSubscriptionWithIAPExpire(ctx context.Context, userSub *user.Subscribe, sub *subscribe.Subscribe, iapExpireAt int64) error { +// updateSubscriptionWithIAPExpire 用于 Apple IAP 续费:按累计加时语义更新到期时间。 +func (l *ActivateOrderLogic) updateSubscriptionWithIAPExpire(ctx context.Context, userSub *user.Subscribe, sub *subscribe.Subscribe, orderInfo *order.Order, iapExpireAt int64) error { now := time.Now() - newExpire := time.Unix(iapExpireAt, 0) + baseTime := userSub.ExpireTime + if baseTime.Before(now) { + baseTime = now + } + newExpire := tool.AddTime(sub.UnitTime, orderInfo.Quantity, baseTime) + if iapExpireAt > 0 { + appleExpire := time.Unix(iapExpireAt, 0) + if appleExpire.After(newExpire) { + newExpire = appleExpire + } + } today := now.Day() resetDay := newExpire.Day()