package apple import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "strings" "time" "github.com/google/uuid" "github.com/hibiken/asynq" commonLogic "github.com/perfect-panel/server/internal/logic/common" iapmodel "github.com/perfect-panel/server/internal/model/iap/apple" ordermodel "github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/internal/model/subscribe" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/constant" 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" ) type AttachTransactionLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } const ( orderTypeSubscribe uint8 = 1 orderStatusPending uint8 = 1 orderStatusPaid uint8 = 2 orderStatusClosed uint8 = 3 orderStatusFinished uint8 = 5 ) func NewAttachTransactionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AttachTransactionLogic { return &AttachTransactionLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest) (*types.AttachAppleTransactionResponse, error) { l.Infow("开始绑定 Apple IAP 交易", logger.Field("orderNo", req.OrderNo)) u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok || u == nil { l.Errorw("无效访问,用户信息缺失") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "invalid access") } // 解析家庭权益:member 的订阅归属于 owner entitlement, entErr := commonLogic.ResolveEntitlementUser(l.ctx, l.svcCtx.DB, u.Id) if entErr != nil { l.Errorw("解析家庭权益失败", logger.Field("userId", u.Id), logger.Field("error", entErr.Error())) return nil, entErr } if strings.TrimSpace(req.OrderNo) == "" { l.Errorw("参数错误,orderNo 不能为空") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "order_no is required") } orderInfo, orderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) switch { case errors.Is(orderErr, gorm.ErrRecordNotFound): l.Errorw("订单不存在", logger.Field("orderNo", req.OrderNo)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist") case orderErr != nil: l.Errorw("查询订单失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", orderErr.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", orderErr.Error()) case orderInfo == nil || orderInfo.Id == 0: l.Errorw("订单不存在", logger.Field("orderNo", req.OrderNo)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist") case orderInfo.UserId != u.Id: l.Errorw("订单与当前用户不匹配", logger.Field("orderNo", req.OrderNo), logger.Field("orderUserId", orderInfo.UserId), logger.Field("userId", u.Id)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "order owner mismatch") } isNewPurchaseOrder := orderInfo.Type == orderTypeSubscribe if isNewPurchaseOrder { l.Infow("首购订单将只由订单激活流程创建订阅", logger.Field("orderNo", req.OrderNo), logger.Field("orderType", orderInfo.Type)) } txPayload, err := iapapple.VerifyTransactionJWS(req.SignedTransactionJWS) if err != nil { l.Errorw("JWS 验签失败", logger.Field("error", err.Error())) 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) existingOrderNo, validateErr := l.validateOrderTradeNoBinding(orderInfo, tradeNoCandidates) if validateErr != nil { l.Errorw("Apple 交易绑定校验失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNoCandidates", tradeNoCandidates), logger.Field("error", validateErr.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "apple transaction binding error") } if existingOrderNo != "" { l.Errorw("Apple 交易重复绑定,返回已绑定订单", logger.Field("orderNo", req.OrderNo), logger.Field("existingOrderNo", existingOrderNo), logger.Field("tradeNoCandidates", tradeNoCandidates)) // 关闭当前 pending 订单,避免产生孤儿订单 if orderInfo.Status == orderStatusPending { if closeErr := l.svcCtx.DB.Model(&ordermodel.Order{}). Where("order_no = ? AND status = ?", req.OrderNo, orderStatusPending). Update("status", orderStatusClosed).Error; closeErr != nil { l.Errorw("关闭重复订单失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", closeErr.Error())) } else { l.Infow("已关闭重复 pending 订单", logger.Field("orderNo", req.OrderNo), logger.Field("existingOrderNo", existingOrderNo)) } } return &types.AttachAppleTransactionResponse{ExistingOrderNo: existingOrderNo}, nil } 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, existTxErr := iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txPayload.OriginalTransactionId) if existTxErr != nil && !errors.Is(existTxErr, gorm.ErrRecordNotFound) { l.Errorw("查询 IAP 事务记录失败", logger.Field("error", existTxErr.Error()), logger.Field("originalTransactionId", txPayload.OriginalTransactionId)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find iap transaction error: %v", existTxErr.Error()) } l.Infow("幂等等检查", logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("exists", existTx != nil && existTx.Id > 0)) // 解析 Apple 商品ID中的单位与数量:支持 dayN / monthN / yearN var parsedUnit string var parsedQuantity int64 if parsed := iapapple.ParseProductIdDuration(txPayload.ProductId); parsed != nil { parsedUnit = parsed.Unit parsedQuantity = parsed.Quantity } l.Infow("商品映射解析", logger.Field("productId", txPayload.ProductId), logger.Field("解析单位", parsedUnit), logger.Field("解析数量", parsedQuantity)) // 基于订阅列表的折扣配置做匹配:UnitTime=Day 且 Discount.quantity == parsedQuantity var duration int64 var tier string var subscribeId int64 if parsedQuantity > 0 { _, subs, e := l.svcCtx.SubscribeModel.FilterList(l.ctx, &subscribe.FilterParams{ Page: 1, Size: 9999, Show: true, Sell: true, DefaultLanguage: true, }) if e == nil && len(subs) > 0 { for _, item := range subs { if parsedUnit != "" && !strings.EqualFold(item.UnitTime, parsedUnit) { continue } var discounts []types.SubscribeDiscount if item.Discount != "" { _ = json.Unmarshal([]byte(item.Discount), &discounts) } for _, d := range discounts { if int64(d.Quantity) == parsedQuantity { switch parsedUnit { case "Day": duration = parsedQuantity case "Month": duration = parsedQuantity * 30 case "Year": duration = parsedQuantity * 365 default: duration = parsedQuantity } subscribeId = item.Id tier = item.Name l.Infow("订阅映射命中", logger.Field("subscribeId", subscribeId), logger.Field("name", tier), logger.Field("durationDays", duration)) break } } if subscribeId > 0 { break } } } else { l.Infow("订阅列表为空或查询失败", logger.Field("error", func() string { if e != nil { return e.Error() } return "" }())) } } if subscribeId == 0 { // fallback from order_no if provided duration = orderInfo.Quantity subscribeId = orderInfo.SubscribeId l.Infow("使用订单信息回退", logger.Field("orderNo", req.OrderNo), logger.Field("durationDays", duration), logger.Field("subscribeId", subscribeId)) // final fallback: use request fields if duration <= 0 { duration = req.DurationDays } if tier == "" { tier = req.Tier } if subscribeId <= 0 { subscribeId = req.SubscribeId } l.Infow("使用请求参数回退", logger.Field("durationDays", duration), logger.Field("tier", tier), logger.Field("subscribeId", subscribeId)) if duration <= 0 || subscribeId <= 0 { l.Errorw("商品识别失败", logger.Field("durationDays", duration), logger.Field("tier", tier), logger.Field("subscribeId", subscribeId)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unknown product") } } 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 != "" { orderSub, subErr := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, orderInfo.SubscribeToken) switch { case subErr == nil && orderSub != nil && orderSub.Id > 0: if orderSub.UserId != u.Id && orderSub.UserId != entitlement.EffectiveUserID { l.Errorw("订单订阅与当前用户不匹配", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("orderSubUserId", orderSub.UserId), logger.Field("userId", u.Id), logger.Field("effectiveUserId", entitlement.EffectiveUserID)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "order subscribe owner mismatch") } orderLinkedSub = orderSub subscribeId = orderSub.SubscribeId l.Infow("IAP 绑定命中订单订阅", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("userSubscribeId", orderSub.Id), logger.Field("subscribeToken", orderInfo.SubscribeToken)) case subErr != nil && !errors.Is(subErr, gorm.ErrRecordNotFound): l.Errorw("查询订单订阅失败", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("subscribeToken", orderInfo.SubscribeToken), logger.Field("error", subErr.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order subscribe error: %v", subErr.Error()) } } var singleModeAnchorSub *user.Subscribe if !isNewPurchaseOrder && l.svcCtx.Config.Subscribe.SingleModel && orderLinkedSub == nil { anchorSub, anchorErr := findSingleModeMergeTarget(l.ctx, l.svcCtx, entitlement.EffectiveUserID, subscribeId) switch { case errors.Is(anchorErr, commonLogic.ErrSingleModePlanMismatch): l.Errorw("单订阅模式下 IAP 套餐不匹配", logger.Field("userId", u.Id), logger.Field("orderNo", req.OrderNo), logger.Field("iapSubscribeId", subscribeId)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.SingleSubscribePlanMismatch), "single subscribe mode plan mismatch") case anchorErr == nil && anchorSub != nil && anchorSub.Id > 0: singleModeAnchorSub = anchorSub subscribeId = anchorSub.SubscribeId l.Infow("IAP 绑定命中单订阅锚点", logger.Field("userSubscribeId", anchorSub.Id), logger.Field("subscribeId", anchorSub.SubscribeId)) case errors.Is(anchorErr, gorm.ErrRecordNotFound): case anchorErr != nil: l.Errorw("查询单订阅锚点失败", logger.Field("userId", u.Id), logger.Field("error", anchorErr.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find single mode anchor subscribe error: %v", anchorErr.Error()) } } 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.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.Infow("事务已处理,首购订单等待激活队列发放订阅", logger.Field("orderNo", req.OrderNo), logger.Field("expiresAt", exp.Unix())) return &types.AttachAppleTransactionResponse{ExpiresAt: exp.Unix(), Tier: tier}, nil } switch { 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, 0); 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.Infow("事务已处理,刷新订阅到期时间", logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("tier", tier), logger.Field("expiresAt", newExpire.Unix())) return &types.AttachAppleTransactionResponse{ ExpiresAt: newExpire.Unix(), Tier: tier, }, nil } if orderLinkedSub != nil { 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, 0); 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.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, 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, 0); 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.Infow("事务已处理,刷新单订阅锚点到期时间", logger.Field("userSubscribeId", singleModeAnchorSub.Id), logger.Field("expiresAt", newExpire.Unix())) return &types.AttachAppleTransactionResponse{ExpiresAt: newExpire.Unix(), Tier: tier}, nil } } sum := sha256.Sum256([]byte(req.SignedTransactionJWS)) jwsHash := hex.EncodeToString(sum[:]) l.Infow("准备写入事务记录", logger.Field("userId", u.Id), logger.Field("transactionId", txPayload.TransactionId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("productId", txPayload.ProductId), logger.Field("jwsHash", jwsHash)) iapTx := &iapmodel.Transaction{ UserId: u.Id, OriginalTransactionId: txPayload.OriginalTransactionId, TransactionId: txPayload.TransactionId, ProductId: txPayload.ProductId, PurchaseAt: txPayload.PurchaseDate, RevocationAt: txPayload.RevocationDate, JWSHash: jwsHash, } err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { if existTx == nil || existTx.Id == 0 { if e := tx.Model(&iapmodel.Transaction{}).Create(iapTx).Error; e != nil { l.Errorw("写入事务表失败", logger.Field("error", e.Error())) return e } l.Infow("写入事务表成功", logger.Field("id", iapTx.Id)) } if !isNewPurchaseOrder { merged := false if orderLinkedSub != nil { // 不在此处更新 expire_time:由激活队列统一写入,避免双重叠加天数 merged = true } else if singleModeAnchorSub != nil { // 同上 merged = true } if !merged { userSub := user.Subscribe{ UserId: entitlement.EffectiveUserID, SubscribeId: subscribeId, OrderId: orderInfo.Id, StartTime: time.Now(), ExpireTime: exp, Traffic: 0, Download: 0, Upload: 0, Token: fmt.Sprintf("iap:%s", txPayload.OriginalTransactionId), UUID: uuid.New().String(), Status: 1, } if e := l.svcCtx.UserModel.InsertSubscribe(l.ctx, &userSub, tx); e != nil { l.Errorw("写入用户订阅失败", logger.Field("error", e.Error())) return e } l.Infow("写入用户订阅成功", logger.Field("userId", u.Id), logger.Field("subscribeId", subscribeId), logger.Field("expireUnix", exp.Unix())) } } 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 } return nil }) if err != nil { l.Errorw("绑定事务提交失败", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert error: %v", err.Error()) } // 事务提交后立即清除订阅缓存,避免 App 查到旧数据(激活队列异步执行,存在竞态) if orderLinkedSub != nil { _ = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, orderLinkedSub) } else if singleModeAnchorSub != nil { _ = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, singleModeAnchorSub) } // merged 路径下,exp 仅从购买日算起,需要用已有订阅到期时间 + duration 作为预估值返回前端 responseExpire := exp var mergeSub *user.Subscribe if orderLinkedSub != nil { mergeSub = orderLinkedSub } else if singleModeAnchorSub != nil { mergeSub = singleModeAnchorSub } if mergeSub != nil { base := mergeSub.ExpireTime now := time.Now() if base.Before(now) { base = now } estimated := base.AddDate(0, 0, int(duration)) if estimated.After(responseExpire) { responseExpire = estimated } } l.Infow("绑定完成", logger.Field("userId", u.Id), logger.Field("tier", tier), logger.Field("expiresAt", responseExpire.Unix())) return &types.AttachAppleTransactionResponse{ ExpiresAt: responseExpire.Unix(), Tier: tier, }, nil } func (l *AttachTransactionLogic) syncOrderStatusAndEnqueue(orderInfo *ordermodel.Order, iapExpireAt int64, tx ...*gorm.DB) error { if orderInfo == nil || orderInfo.OrderNo == "" { return errors.New("order info is nil") } if orderInfo.Status == orderStatusPending { if err := l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, orderInfo.OrderNo, orderStatusPaid, tx...); err != nil { return err } orderInfo.Status = orderStatusPaid l.Infow("更新订单状态成功", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("status", orderStatusPaid)) } // enqueue activation regardless (idempotent handler downstream) payload := queueType.ForthwithActivateOrderPayload{OrderNo: orderInfo.OrderNo, IAPExpireAt: iapExpireAt} bytes, _ := json.Marshal(payload) task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes) if _, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task); err != nil { // non-fatal l.Errorw("enqueue activate task error", logger.Field("error", err.Error())) } else { l.Infow("已加入订单激活队列", logger.Field("orderNo", orderInfo.OrderNo)) } return nil } func (l *AttachTransactionLogic) findIAPSubscribeByOriginalTransactionID(originalTransactionID string) (*user.Subscribe, error) { if originalTransactionID == "" { return nil, gorm.ErrRecordNotFound } candidates := []string{fmt.Sprintf("iap:%s", originalTransactionID), originalTransactionID} for _, token := range candidates { sub, err := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, token) if err == nil && sub != nil && sub.Id > 0 { return sub, nil } if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } } return nil, gorm.ErrRecordNotFound } 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 := l.calcIAPRenewalExpire(userSub.ExpireTime, exp, durationDays) userSub.ExpireTime = newExpire if subscribeId > 0 && userSub.SubscribeId != subscribeId { userSub.SubscribeId = subscribeId } userSub.Status = 1 userSub.FinishedAt = nil if err := l.svcCtx.UserModel.UpdateSubscribe(l.ctx, userSub, tx...); err != nil { return time.Time{}, err } 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, 1) if txPayload.TransactionId != "" { candidates = append(candidates, txPayload.TransactionId) } return candidates } // validateOrderTradeNoBinding returns (existingOrderNo, error). // existingOrderNo is non-empty when the Apple transaction is already bound to a paid/finished order. func (l *AttachTransactionLogic) validateOrderTradeNoBinding(orderInfo *ordermodel.Order, tradeNoCandidates []string) (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, []int64{int64(orderStatusPaid), int64(orderStatusFinished)}). Order("id DESC"). First(&boundOrder).Error if errors.Is(err, gorm.ErrRecordNotFound) { return "", nil } if err != nil { return "", err } return boundOrder.OrderNo, nil } 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 }