234 lines
7.1 KiB
Go
234 lines
7.1 KiB
Go
package application
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/dujiao-next/internal/constants"
|
|
"github.com/dujiao-next/internal/logger"
|
|
downstreamcontract "github.com/dujiao-next/internal/modules/downstreamcallback/contract"
|
|
downstreamdomain "github.com/dujiao-next/internal/modules/downstreamcallback/domain"
|
|
)
|
|
|
|
const maxCallbackRetries = 5
|
|
|
|
var callbackRetryDelays = []time.Duration{
|
|
30 * time.Second,
|
|
60 * time.Second,
|
|
120 * time.Second,
|
|
300 * time.Second,
|
|
}
|
|
|
|
// Options 声明下游回调应用服务的全部端口。
|
|
type Options struct {
|
|
References downstreamcontract.Repository
|
|
Orders downstreamcontract.OrderReader
|
|
Credentials downstreamcontract.CredentialReader
|
|
Queue downstreamcontract.CallbackQueue
|
|
Deliverer downstreamcontract.Deliverer
|
|
Now func() time.Time
|
|
}
|
|
|
|
// Service 编排 B 侧下游回调引用、投递和重试状态。
|
|
type Service struct {
|
|
references downstreamcontract.Repository
|
|
orders downstreamcontract.OrderReader
|
|
credentials downstreamcontract.CredentialReader
|
|
queue downstreamcontract.CallbackQueue
|
|
deliverer downstreamcontract.Deliverer
|
|
now func() time.Time
|
|
}
|
|
|
|
// NewService 创建下游回调应用服务。
|
|
func NewService(options Options) *Service {
|
|
if options.References == nil {
|
|
panic("downstream callback service: references are nil")
|
|
}
|
|
if options.Orders == nil {
|
|
panic("downstream callback service: orders are nil")
|
|
}
|
|
if options.Credentials == nil {
|
|
panic("downstream callback service: credentials are nil")
|
|
}
|
|
if options.Deliverer == nil {
|
|
panic("downstream callback service: deliverer is nil")
|
|
}
|
|
if options.Now == nil {
|
|
options.Now = time.Now
|
|
}
|
|
return &Service{
|
|
references: options.References,
|
|
orders: options.Orders,
|
|
credentials: options.Credentials,
|
|
queue: options.Queue,
|
|
deliverer: options.Deliverer,
|
|
now: options.Now,
|
|
}
|
|
}
|
|
|
|
// CreateRef 创建下游订单引用并强制初始化为待回调状态。
|
|
func (s *Service) CreateRef(ref *downstreamdomain.OrderRef) error {
|
|
if ref == nil || ref.OrderID == 0 {
|
|
return downstreamcontract.ErrInvalidRef
|
|
}
|
|
ref.CallbackStatus = downstreamdomain.StatusPending
|
|
return s.references.Create(ref)
|
|
}
|
|
|
|
// GetByOrderID 根据订单 ID 查询下游引用。
|
|
func (s *Service) GetByOrderID(orderID uint) (*downstreamdomain.OrderRef, error) {
|
|
return s.references.GetByOrderID(orderID)
|
|
}
|
|
|
|
// EnqueueCallback 在 B 侧订单状态变更后解析引用并投递回调任务。
|
|
func (s *Service) EnqueueCallback(orderID uint) {
|
|
if s.queue == nil {
|
|
logger.Debugw("downstream_callback_skip_no_queue", "order_id", orderID)
|
|
return
|
|
}
|
|
|
|
ref, err := s.references.GetByOrderID(orderID)
|
|
if err != nil || ref == nil {
|
|
order, orderErr := s.orders.GetByID(orderID)
|
|
if orderErr != nil || order == nil || order.ParentID == nil {
|
|
logger.Debugw("downstream_callback_skip_no_ref", "order_id", orderID, "error", err)
|
|
return
|
|
}
|
|
ref, err = s.references.GetByOrderID(*order.ParentID)
|
|
if err != nil || ref == nil {
|
|
logger.Debugw("downstream_callback_skip_no_ref", "order_id", orderID, "parent_id", *order.ParentID, "error", err)
|
|
return
|
|
}
|
|
logger.Debugw("downstream_callback_resolved_parent_ref", "order_id", orderID, "parent_id", *order.ParentID, "ref_id", ref.ID)
|
|
}
|
|
|
|
if strings.TrimSpace(ref.CallbackURL) == "" {
|
|
logger.Warnw("downstream_callback_skip_empty_url", "order_id", orderID, "ref_id", ref.ID)
|
|
return
|
|
}
|
|
logger.Infow("downstream_callback_enqueue",
|
|
"order_id", orderID,
|
|
"ref_id", ref.ID,
|
|
"callback_url", ref.CallbackURL,
|
|
"callback_status", ref.CallbackStatus,
|
|
)
|
|
if ref.CallbackStatus != downstreamdomain.StatusPending {
|
|
ref.CallbackStatus = downstreamdomain.StatusPending
|
|
ref.CallbackRetryCount = 0
|
|
_ = s.references.Update(ref)
|
|
}
|
|
if err := s.queue.EnqueueCallback(ref.ID, 0); err != nil {
|
|
logger.Warnw("downstream_enqueue_callback_failed", "order_id", orderID, "ref_id", ref.ID, "error", err)
|
|
}
|
|
}
|
|
|
|
// SendCallback 读取最新订单状态并执行一次下游回调。
|
|
func (s *Service) SendCallback(ctx context.Context, refID uint) error {
|
|
ref, err := s.references.GetByID(refID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ref == nil {
|
|
return downstreamcontract.ErrRefNotFound
|
|
}
|
|
if strings.TrimSpace(ref.CallbackURL) == "" {
|
|
return nil
|
|
}
|
|
|
|
order, err := s.orders.GetByID(ref.OrderID)
|
|
if err != nil || order == nil {
|
|
logger.Warnw("downstream_callback_order_not_found", "ref_id", ref.ID, "order_id", ref.OrderID)
|
|
return err
|
|
}
|
|
credential, err := s.credentials.GetByID(ref.ApiCredentialID)
|
|
if err != nil || credential == nil {
|
|
logger.Warnw("downstream_callback_credential_not_found", "ref_id", ref.ID, "credential_id", ref.ApiCredentialID)
|
|
return fmt.Errorf("credential not found for ref %d", ref.ID)
|
|
}
|
|
|
|
event := "order.status_changed"
|
|
if order.Status == constants.OrderStatusDelivered || order.Status == constants.OrderStatusCompleted {
|
|
event = "order.fulfilled"
|
|
}
|
|
fulfillment := deliveredFulfillment(order)
|
|
now := s.now()
|
|
payload := downstreamcontract.CallbackPayload{
|
|
Event: event,
|
|
OrderID: order.ID,
|
|
OrderNo: order.OrderNo,
|
|
DownstreamOrderNo: ref.DownstreamOrderNo,
|
|
Status: order.Status,
|
|
Fulfillment: fulfillment,
|
|
Timestamp: now.Unix(),
|
|
}
|
|
|
|
logger.Infow("downstream_callback_sending",
|
|
"ref_id", ref.ID,
|
|
"order_id", order.ID,
|
|
"callback_url", ref.CallbackURL,
|
|
"event", event,
|
|
"status", order.Status,
|
|
"has_fulfillment", fulfillment != nil,
|
|
)
|
|
if err := s.deliverer.Send(ctx, downstreamcontract.DeliveryRequest{
|
|
URL: ref.CallbackURL,
|
|
APIKey: credential.APIKey,
|
|
APISecret: credential.APISecret,
|
|
Payload: payload,
|
|
}); err != nil {
|
|
logger.Warnw("downstream_callback_http_error", "ref_id", ref.ID, "callback_url", ref.CallbackURL, "error", err)
|
|
return s.handleCallbackFailure(ref, now, err)
|
|
}
|
|
|
|
ref.CallbackStatus = downstreamdomain.StatusSent
|
|
ref.LastCallbackAt = &now
|
|
return s.references.Update(ref)
|
|
}
|
|
|
|
func deliveredFulfillment(order *downstreamcontract.OrderSnapshot) *downstreamcontract.Fulfillment {
|
|
if order == nil {
|
|
return nil
|
|
}
|
|
source := order.Fulfillment
|
|
if source == nil {
|
|
for i := range order.Children {
|
|
if order.Children[i].Fulfillment != nil {
|
|
source = order.Children[i].Fulfillment
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if source == nil || source.Status != constants.FulfillmentStatusDelivered {
|
|
return nil
|
|
}
|
|
copy := *source
|
|
return ©
|
|
}
|
|
|
|
func (s *Service) handleCallbackFailure(ref *downstreamdomain.OrderRef, now time.Time, callbackErr error) error {
|
|
ref.CallbackRetryCount++
|
|
ref.LastCallbackAt = &now
|
|
|
|
if ref.CallbackRetryCount >= maxCallbackRetries {
|
|
ref.CallbackStatus = downstreamdomain.StatusFailed
|
|
logger.Warnw("downstream_callback_max_retries",
|
|
"ref_id", ref.ID,
|
|
"order_id", ref.OrderID,
|
|
"retry_count", ref.CallbackRetryCount,
|
|
"error", callbackErr,
|
|
)
|
|
} else if s.queue != nil {
|
|
index := ref.CallbackRetryCount - 1
|
|
if index >= len(callbackRetryDelays) {
|
|
index = len(callbackRetryDelays) - 1
|
|
}
|
|
if err := s.queue.EnqueueCallback(ref.ID, callbackRetryDelays[index]); err != nil {
|
|
logger.Warnw("downstream_callback_requeue_failed", "ref_id", ref.ID, "error", err)
|
|
}
|
|
}
|
|
|
|
return s.references.Update(ref)
|
|
}
|