package iap import ( "context" "encoding/json" "strings" "time" "github.com/hibiken/asynq" "github.com/perfect-panel/server/internal/model/payment" "github.com/perfect-panel/server/internal/svc" iapapple "github.com/perfect-panel/server/pkg/iap/apple" "github.com/perfect-panel/server/pkg/logger" pkgpayment "github.com/perfect-panel/server/pkg/payment" queueTypes "github.com/perfect-panel/server/queue/types" ) // ReconcileLogic 第二层:每 5 分钟扫描 trade_no 非空的待支付 IAP 订单 type ReconcileLogic struct { svc *svc.ServiceContext } func NewReconcileLogic(svc *svc.ServiceContext) *ReconcileLogic { return &ReconcileLogic{svc: svc} } func (l *ReconcileLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error { logger.Infof("[IAPReconcile] start at %s", time.Now().Format("2006-01-02 15:04:05")) return l.reconcile(ctx, 5*time.Minute, 48*time.Hour, true) } // reconcile 核心对账逻辑,被第二层和第三层共享 func (l *ReconcileLogic) reconcile(ctx context.Context, minAge, maxAge time.Duration, requireTradeNo bool) error { apiCfg, err := l.loadAppleAPIConfig(ctx) if err != nil { logger.Errorf("[IAPReconcile] load apple config error: %v", err) return nil } if apiCfg == nil { logger.Infof("[IAPReconcile] no enabled apple iap payment found, skip") return nil } orders, err := l.svc.OrderModel.FindPendingIAPOrders(ctx, minAge, maxAge, requireTradeNo) if err != nil { logger.Errorf("[IAPReconcile] find pending orders error: %v", err) return nil } if len(orders) == 0 { return nil } logger.Infof("[IAPReconcile] found %d pending IAP orders (requireTradeNo=%v)", len(orders), requireTradeNo) for _, ord := range orders { if ord.TradeNo == "" { continue } // 用 originalTransactionId(即 trade_no)向 Apple Server API 查询交易详情 jws, e := iapapple.GetTransactionInfo(*apiCfg, ord.TradeNo) if e != nil { logger.Errorf("[IAPReconcile] GetTransactionInfo error: orderNo=%s tradeNo=%s err=%v", ord.OrderNo, ord.TradeNo, e) time.Sleep(100 * time.Millisecond) continue } // 解析 JWS(不校验证书链,只读取 payload) txPayload, e := iapapple.ParseTransactionJWS(jws) if e != nil { logger.Errorf("[IAPReconcile] ParseTransactionJWS error: orderNo=%s err=%v", ord.OrderNo, e) continue } if txPayload.RevocationDate != nil { // 苹果已撤销交易 → 关闭订单 logger.Infof("[IAPReconcile] transaction revoked, closing order: %s", ord.OrderNo) _ = l.svc.OrderModel.UpdateOrderStatus(ctx, ord.OrderNo, 3) continue } // 正常已付款交易 → enqueue 激活(activateOrderLogic 内部有幂等保护) payload, _ := json.Marshal(queueTypes.ForthwithActivateOrderPayload{OrderNo: ord.OrderNo}) task := asynq.NewTask(queueTypes.ForthwithActivateOrder, payload) if _, e = l.svc.Queue.EnqueueContext(ctx, task); e != nil { logger.Errorf("[IAPReconcile] enqueue activate error: orderNo=%s err=%v", ord.OrderNo, e) } else { logger.Infof("[IAPReconcile] enqueued activate: %s", ord.OrderNo) } time.Sleep(100 * time.Millisecond) // 避免 Apple API 限频 } return nil } // loadAppleAPIConfig 从 payment 表读取第一个启用的 Apple IAP 支付方式配置 func (l *ReconcileLogic) loadAppleAPIConfig(ctx context.Context) (*iapapple.ServerAPIConfig, error) { pays, err := l.svc.PaymentModel.FindListByPlatform(ctx, pkgpayment.AppleIAP.String()) if err != nil { return nil, err } for _, pay := range pays { if pay.Enable == nil || !*pay.Enable || pay.Config == "" { continue } var cfg payment.AppleIAPConfig if e := cfg.Unmarshal([]byte(pay.Config)); e != nil { continue } if cfg.KeyID == "" || cfg.IssuerID == "" || cfg.PrivateKey == "" { continue } apiCfg := &iapapple.ServerAPIConfig{ KeyID: cfg.KeyID, IssuerID: cfg.IssuerID, PrivateKey: fixPEM(cfg.PrivateKey), Sandbox: cfg.Sandbox, } // 从 site custom data 读取 BundleID var customData struct { IapBundleId string `json:"iapBundleId"` } if l.svc.Config.Site.CustomData != "" { _ = json.Unmarshal([]byte(l.svc.Config.Site.CustomData), &customData) apiCfg.BundleID = customData.IapBundleId } return apiCfg, nil } return nil, nil } func fixPEM(key string) string { if !strings.Contains(key, "\n") && strings.Contains(key, "BEGIN PRIVATE KEY") { key = strings.ReplaceAll(key, " ", "\n") key = strings.ReplaceAll(key, "-----BEGIN\nPRIVATE\nKEY-----", "-----BEGIN PRIVATE KEY-----") key = strings.ReplaceAll(key, "-----END\nPRIVATE\nKEY-----", "-----END PRIVATE KEY-----") } return key }