From a0ae7b1c8dbf9f2024697399013526797937fee5 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Sat, 7 Mar 2026 05:54:49 -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 --- internal/logic/notify/appleIAPNotifyLogic.go | 42 +++++++++ .../iap/apple/attachTransactionByIdLogic.go | 18 +--- .../logic/public/iap/apple/getStatusLogic.go | 51 +++++----- .../logic/public/iap/apple/restoreLogic.go | 92 ++++++++++++++++--- 4 files changed, 150 insertions(+), 53 deletions(-) diff --git a/internal/logic/notify/appleIAPNotifyLogic.go b/internal/logic/notify/appleIAPNotifyLogic.go index 0770db8..19e25f8 100644 --- a/internal/logic/notify/appleIAPNotifyLogic.go +++ b/internal/logic/notify/appleIAPNotifyLogic.go @@ -8,6 +8,7 @@ import ( 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" iapapple "github.com/perfect-panel/server/pkg/iap/apple" @@ -164,6 +165,47 @@ func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error { } token := "iap:" + txPayload.OriginalTransactionId sub, e := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, token) + if (e != nil || sub == nil || sub.Id == 0) && existing != nil && existing.UserId > 0 && days > 0 { + // 首购订单创建的订阅 token 不是 iap: 前缀,尝试按 userId+subscribeId 查找 + l.Infow("iap notify fallback: find subscribe by userId", logger.Field("userId", existing.UserId)) + userSubs, queryErr := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, existing.UserId, 0, 1, 2, 3) + if queryErr == nil { + exp := iapapple.CalcExpire(txPayload.PurchaseDate, days) + for _, us := range userSubs { + if us == nil { + continue + } + candidate := &user.Subscribe{ + Id: us.Id, + UserId: us.UserId, + SubscribeId: us.SubscribeId, + ExpireTime: us.ExpireTime, + Status: us.Status, + Token: us.Token, + FinishedAt: us.FinishedAt, + } + if txPayload.RevocationDate != nil { + candidate.Status = 3 + t := *txPayload.RevocationDate + candidate.FinishedAt = &t + candidate.ExpireTime = t + } else { + if exp.After(candidate.ExpireTime) { + candidate.ExpireTime = exp + } + candidate.Status = 1 + candidate.FinishedAt = nil + } + if err := l.svcCtx.UserModel.UpdateSubscribe(l.ctx, candidate, db); err != nil { + l.Errorw("iap notify fallback update subscribe error", logger.Field("error", err.Error()), logger.Field("userSubscribeId", candidate.Id)) + return err + } + l.Infow("iap notify fallback updated subscribe", logger.Field("userSubscribeId", candidate.Id), logger.Field("status", candidate.Status)) + break + } + } + return nil + } if e == nil && sub != nil && sub.Id != 0 { if txPayload.RevocationDate != nil { // 撤销:订阅置为过期并记录完成时间 diff --git a/internal/logic/public/iap/apple/attachTransactionByIdLogic.go b/internal/logic/public/iap/apple/attachTransactionByIdLogic.go index fbf8843..f6dc219 100644 --- a/internal/logic/public/iap/apple/attachTransactionByIdLogic.go +++ b/internal/logic/public/iap/apple/attachTransactionByIdLogic.go @@ -83,27 +83,11 @@ func (l *AttachTransactionByIdLogic) AttachById(req *types.AttachAppleTransactio apiCfg.PrivateKey = strings.ReplaceAll(apiCfg.PrivateKey, "-----END\nPRIVATE\nKEY-----", "-----END PRIVATE KEY-----") } - // Fallback to hardcoded key (For debugging/dev) - if apiCfg.PrivateKey == "" { - apiCfg.PrivateKey = `-----BEGIN PRIVATE KEY----- -MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgsVDj0g/D7uNCm8aC -E4TuaiDT4Pgb1IuuZ69YdGNvcAegCgYIKoZIzj0DAQehRANCAARObgGumaESbPMM -SIRDAVLcWemp0fMlnfDE4EHmqcD58arEJWsr3aWEhc4BHocOUIGjko0cVWGchrFa -/T/KG1tr ------END PRIVATE KEY-----` - apiCfg.KeyID = "2C4X3HVPM8" - } - if apiCfg.KeyID == "" || apiCfg.IssuerID == "" || apiCfg.PrivateKey == "" { - l.Errorw("attach by id credential missing") + l.Errorw("attach by id credential missing", logger.Field("keyID", apiCfg.KeyID), logger.Field("issuerID", apiCfg.IssuerID), logger.Field("hasPrivateKey", apiCfg.PrivateKey != "")) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "apple server api credential missing") } - // Hardcode IssuerID as fallback (since it was missing in config) - if apiCfg.IssuerID == "" || apiCfg.IssuerID == "some_issuer_id" { - apiCfg.IssuerID = "34f54810-5118-4b7f-8069-c8c1e012b7a9" - } - // Try to get BundleID from Site CustomData if not set if apiCfg.BundleID == "" { var customData struct { diff --git a/internal/logic/public/iap/apple/getStatusLogic.go b/internal/logic/public/iap/apple/getStatusLogic.go index 3ee6f62..524a155 100644 --- a/internal/logic/public/iap/apple/getStatusLogic.go +++ b/internal/logic/public/iap/apple/getStatusLogic.go @@ -2,15 +2,14 @@ package apple import ( "context" + "strings" "time" - iapmodel "github.com/perfect-panel/server/internal/model/iap/apple" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/xerr" - iapapple "github.com/perfect-panel/server/pkg/iap/apple" - "github.com/perfect-panel/server/pkg/constant" "github.com/pkg/errors" ) @@ -33,30 +32,36 @@ func (l *GetStatusLogic) GetStatus() (*types.GetAppleStatusResponse, error) { if !ok || u == nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "invalid access") } - pm, _ := iapapple.ParseProductMap(l.svcCtx.Config.Site.CustomData) - var latest *iapmodel.Transaction - var err error - for pid := range pm.Items { - item, e := iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByUserAndProduct(l.ctx, u.Id, pid) - if e == nil && item != nil && item.Id != 0 { - if latest == nil || item.PurchaseAt.After(latest.PurchaseAt) { - latest = item + // 查该用户所有状态的订阅,找 IAP 相关的(token 以 iap: 开头) + subs, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 1) + if err != nil { + return &types.GetAppleStatusResponse{Active: false, ExpiresAt: 0, Tier: ""}, nil + } + now := time.Now() + var bestExpire time.Time + var bestTier string + for _, sub := range subs { + if sub == nil { + continue + } + // 仅处理 IAP 创建的订阅(token 以 "iap:" 开头) + if !strings.HasPrefix(sub.Token, "iap:") { + continue + } + if sub.ExpireTime.After(bestExpire) { + bestExpire = sub.ExpireTime + if sub.Subscribe != nil { + bestTier = sub.Subscribe.Name } } } - if latest == nil { - return &types.GetAppleStatusResponse{ - Active: false, - ExpiresAt: 0, - Tier: "", - }, nil + if bestExpire.IsZero() { + return &types.GetAppleStatusResponse{Active: false, ExpiresAt: 0, Tier: ""}, nil } - m := pm.Items[latest.ProductId] - exp := iapapple.CalcExpire(latest.PurchaseAt, m.DurationDays).Unix() - active := latest.RevocationAt == nil && (exp == 0 || exp > time.Now().Unix()) + active := bestExpire.After(now) return &types.GetAppleStatusResponse{ Active: active, - ExpiresAt: exp, - Tier: m.Tier, - }, err + ExpiresAt: bestExpire.Unix(), + Tier: bestTier, + }, nil } diff --git a/internal/logic/public/iap/apple/restoreLogic.go b/internal/logic/public/iap/apple/restoreLogic.go index 78b322c..3e15c1d 100644 --- a/internal/logic/public/iap/apple/restoreLogic.go +++ b/internal/logic/public/iap/apple/restoreLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strconv" "strings" "time" @@ -11,6 +12,7 @@ import ( commonLogic "github.com/perfect-panel/server/internal/logic/common" iapmodel "github.com/perfect-panel/server/internal/model/iap/apple" "github.com/perfect-panel/server/internal/model/payment" + "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" @@ -71,18 +73,10 @@ func (l *RestoreLogic) Restore(req *types.RestoreAppleTransactionsRequest) error } } - // Fallback credentials if missing (dev/debug) - if apiCfg.PrivateKey == "" { - apiCfg.PrivateKey = `-----BEGIN PRIVATE KEY----- -MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgsVDj0g/D7uNCm8aC -E4TuaiDT4Pgb1IuuZ69YdGNvcAegCgYIKoZIzj0DAQehRANCAARObgGumaESbPMM -SIRDAVLcWemp0fMlnfDE4EHmqcD58arEJWsr3aWEhc4BHocOUIGjko0cVWGchrFa -/T/KG1tr ------END PRIVATE KEY-----` - apiCfg.KeyID = "2C4X3HVPM8" - } - if apiCfg.IssuerID == "" { - apiCfg.IssuerID = "34f54810-5118-4b7f-8069-c8c1e012b7a9" + // Fallback credentials if missing + if apiCfg.PrivateKey == "" || apiCfg.KeyID == "" || apiCfg.IssuerID == "" { + l.Errorw("restore: apple server api credential missing", logger.Field("keyID", apiCfg.KeyID), logger.Field("issuerID", apiCfg.IssuerID), logger.Field("hasPrivateKey", apiCfg.PrivateKey != "")) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "apple server api credential missing") } // Try to get BundleID if apiCfg.BundleID == "" && l.svcCtx.Config.Site.CustomData != "" { @@ -118,7 +112,79 @@ SIRDAVLcWemp0fMlnfDE4EHmqcD58arEJWsr3aWEhc4BHocOUIGjko0cVWGchrFa m, ok := pm.Items[txp.ProductId] if !ok { - continue + // fallback: 按命名约定(day/month/year + 数字)从订阅列表匹配 + var parsedUnit string + var parsedQuantity int64 + pid := strings.ToLower(txp.ProductId) + parts := strings.Split(pid, ".") + for i := len(parts) - 1; i >= 0; i-- { + p := parts[i] + 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"):] + default: + continue + } + 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 + } + } + 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 { + 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 { + var dur int64 + switch parsedUnit { + case "Day": + dur = parsedQuantity + case "Month": + dur = parsedQuantity * 30 + case "Year": + dur = parsedQuantity * 365 + default: + dur = parsedQuantity + } + m = iapapple.ProductMapping{DurationDays: dur, Tier: item.Name, SubscribeId: item.Id} + ok = true + break + } + } + if ok { + break + } + } + } + } + if !ok { + l.Errorw("restore: product mapping not found", logger.Field("productId", txp.ProductId)) + continue + } } // Check if already processed _, e := iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txp.OriginalTransactionId)