Files
ttstd-infinite-canvas/backend/internal/app/payment_worker.go

119 lines
3.1 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.
// 支付后台 worker:消费异步通知、关闭过期订单、补查待支付订单。
//
// 通知与补查走同一条 applyPaymentResult 幂等入账路径,重复通知不会重复加积分。
package app
import (
"context"
"errors"
"fmt"
"log"
"math"
"time"
"infinite-canvas/backend/internal/payment"
)
func (s *Service) startPaymentWorker(ctx context.Context) {
s.runWorkerLoop(func(ctx context.Context) {
s.drainPaymentNotifications()
notificationTicker := time.NewTicker(15 * time.Second)
defer notificationTicker.Stop()
for {
select {
case <-ctx.Done():
return
case <-notificationTicker.C:
s.drainPaymentNotifications()
}
}
})
s.runWorkerLoop(func(ctx context.Context) {
s.reconcileExpiredPaymentOrders(ctx)
orderTicker := time.NewTicker(15 * time.Second)
defer orderTicker.Stop()
for {
select {
case <-ctx.Done():
return
case <-orderTicker.C:
s.reconcileExpiredPaymentOrders(ctx)
s.queryPendingPaymentOrders(ctx)
}
}
})
s.runWorkerLoop(func(ctx context.Context) {
s.maybeRunDailyPaymentReconciliation(ctx)
reconciliationTicker := time.NewTicker(30 * time.Minute)
defer reconciliationTicker.Stop()
for {
select {
case <-ctx.Done():
return
case <-reconciliationTicker.C:
s.maybeRunDailyPaymentReconciliation(ctx)
}
}
})
}
func (s *Service) drainPaymentNotifications() {
items, err := s.repo.PendingPaymentNotifications(32)
if err != nil {
log.Printf("payment notification query failed: %v", err)
return
}
for index := range items {
item := &items[index]
if err := s.processPaymentNotification(item); err != nil {
delay := time.Duration(math.Pow(2, math.Min(float64(item.Attempts), 8))) * 5 * time.Second
_ = s.repo.RetryPaymentNotification(item.ID, safePaymentError(err), time.Now().Add(delay))
}
}
}
func (s *Service) reconcileExpiredPaymentOrders(ctx context.Context) {
orders, err := s.repo.ClaimExpiredPaymentOrders(32)
if err != nil {
log.Printf("expired payment order claim failed: %v", err)
return
}
for index := range orders {
order := &orders[index]
operationContext, cancel := context.WithTimeout(ctx, 45*time.Second)
err := s.closePaymentOrder(operationContext, order)
cancel()
if err != nil {
_ = s.repo.RestoreClosingPaymentOrder(order.ID, safePaymentError(err))
}
}
}
func (s *Service) queryPendingPaymentOrders(ctx context.Context) {
orders, err := s.repo.PaymentOrdersNeedingQuery(time.Now().Add(-30*time.Second), 32)
if err != nil {
log.Printf("pending payment order query failed: %v", err)
return
}
for index := range orders {
operationContext, cancel := context.WithTimeout(ctx, 20*time.Second)
err := s.queryPaymentOrder(operationContext, &orders[index])
cancel()
if err != nil {
log.Printf("payment order compensation query failed: order=%s error_type=%T", orders[index].ID, err)
}
}
}
func safePaymentError(err error) string {
if err == nil {
return ""
}
var providerErr *payment.ProviderError
if errors.As(err, &providerErr) && providerErr.Code != "" {
return truncateRunes(providerErr.Code, 1000)
}
return truncateRunes(fmt.Sprintf("%T", err), 1000)
}