hi-server/queue/logic/iap/reconcileLogic.go
shanshanzhong 7a3a53f1a9
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m47s
ipa
2026-03-08 05:12:28 -07:00

135 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}