package apple import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "strconv" "strings" "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/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 } 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") } 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)) // 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) 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 { pid := strings.ToLower(txPayload.ProductId) parts := strings.Split(pid, ".") for i := len(parts) - 1; i >= 0; i-- { p := parts[i] if strings.HasPrefix(p, "day") || strings.HasPrefix(p, "month") || strings.HasPrefix(p, "year") { switch { case strings.HasPrefix(p, "day"): parsedUnit = "Day" p = p[len("day"):] case strings.HasPrefix(p, "month"): parsedUnit = "Month" p = p[len("month"):] case strings.HasPrefix(p, "year"): parsedUnit = "Year" p = p[len("year"):] } digits := p for j := 0; j < len(digits); j++ { if digits[j] < '0' || digits[j] > '9' { digits = digits[:j] break } } if q, e := strconv.ParseInt(digits, 10, 64); e == nil && q > 0 { parsedQuantity = q break } } } } 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 if req.OrderNo != "" { if ord, e := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo); e == nil && ord != nil && ord.Id != 0 { duration = ord.Quantity subscribeId = ord.SubscribeId l.Infow("使用订单信息回退", logger.Field("orderNo", req.OrderNo), logger.Field("durationDays", duration), logger.Field("subscribeId", subscribeId)) } else { l.Infow("订单信息不可用,尝试请求参数回退", logger.Field("orderNo", req.OrderNo)) } } // 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) l.Infow("计算订阅到期时间", logger.Field("expireAt", exp), logger.Field("expireUnix", exp.Unix())) if existTx != nil && existTx.Id > 0 { token := fmt.Sprintf("iap:%s", txPayload.OriginalTransactionId) existSub, err := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, token) if err == nil && existSub != nil && existSub.Id > 0 { // Already processed, return success l.Infow("事务已处理,直接返回", logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("tier", tier), logger.Field("expiresAt", exp.Unix())) return &types.AttachAppleTransactionResponse{ ExpiresAt: exp.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)) } // insert user_subscribe userSub := user.Subscribe{ UserId: u.Id, SubscribeId: subscribeId, 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())) // 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 l.Infow("订单不存在或查询失败,跳过订单状态更新", logger.Field("orderNo", req.OrderNo)) return nil } if orderInfo.Status == 1 { if e := l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, req.OrderNo, 2, tx); e != nil { l.Errorw("更新订单状态失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", e.Error())) return e } l.Infow("更新订单状态成功", logger.Field("orderNo", req.OrderNo), logger.Field("status", 2)) } // 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())) } else { l.Infow("已加入订单激活队列", logger.Field("orderNo", req.OrderNo)) } } 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()) } l.Infow("绑定完成", logger.Field("userId", u.Id), logger.Field("tier", tier), logger.Field("expiresAt", exp.Unix())) return &types.AttachAppleTransactionResponse{ ExpiresAt: exp.Unix(), Tier: tier, }, nil }