package service import ( "context" "errors" "net/http" "strings" "time" "affiliate_dash/internal/model" "affiliate_dash/internal/pkg/timeutil" "gorm.io/gorm" "gorm.io/gorm/clause" ) func (s *DeliveryService) Submit(orderNo, gameAccount, bindUUID string, auth DeliveryLinkAuth) (*DeliverySubmitResult, error) { return s.submit(orderNo, gameAccount, bindUUID, 0, &auth, 0) } func (s *DeliveryService) SubmitForMerchant(merchantID, apiClientID uint, orderNo, gameAccount, bindUUID string) (*DeliverySubmitResult, error) { return s.submit(orderNo, gameAccount, bindUUID, apiClientID, nil, merchantID) } func (s *DeliveryService) submit(orderNo, gameAccount, bindUUID string, apiClientID uint, auth *DeliveryLinkAuth, merchantID uint) (*DeliverySubmitResult, error) { gameAccount = strings.TrimSpace(gameAccount) bindUUID = strings.TrimSpace(bindUUID) orderNo = strings.TrimSpace(orderNo) if gameAccount == "" || bindUUID == "" { return nil, errors.New("玩家编号和绑定凭证不能为空") } var ( openOrder *OpenOrderQuery order *model.FulfillmentOrder goodID string err error ) if merchantID > 0 { openOrder, order, goodID, err = s.loadMerchantOrder(merchantID, orderNo) } else { openOrder, order, goodID, err = s.loadLinkedOrder(orderNo, auth) } if err != nil { return nil, err } info, _, _, err := s.buildDeliveryState(openOrder, order, goodID, false) if err != nil { return nil, err } orderStatus := normalizeOrderStatus(order) if orderStatus == model.OrderStatusDelivering || orderStatus == model.OrderStatusDelivered || strings.TrimSpace(order.ProviderOrderNo) != "" || strings.Contains(order.ResultData, "\"queue_order_id\"") || strings.Contains(order.ResultData, "\"lewan_order_id\"") { if existing := buildExistingDeliverySubmitResult(order); existing != nil { return existing, nil } } // 已在上游创建正式订单的失败订单禁止自动重发,避免重复发货。 if orderStatus == model.OrderStatusShipFailed && deliverySubmittedUpstream(order) { return nil, newDeliveryHTTPError(http.StatusConflict, "该订单已进入发货流程,为避免重复发货不能直接重试,请稍后查询订单状态或联系平台处理") } if !info.CanShip { reason := info.CannotShipReason if reason == "" { reason = "订单暂不可发货" } return nil, newDeliveryHTTPError(http.StatusBadRequest, reason) } claimed, claimedNow, err := s.claimDeliverySubmission(order.MerchantID, apiClientID, order.OrderNo) if err != nil { return nil, err } if !claimedNow { if existing := buildExistingDeliverySubmitResult(claimed); existing != nil { return existing, nil } return nil, newDeliveryHTTPError(http.StatusConflict, "订单暂时正在发货中,请稍后查询") } providers, _ := s.router.ResolveProviders(claimed) if len(providers) == 0 { s.markDeliverySubmissionFailed(claimed, apiClientID, deliveryStageCreateOrder, "未配置发货渠道") return nil, errors.New("未配置发货渠道") } activeProvider := providers[0] submitRes, err := activeProvider.Submit(context.Background(), claimed, gameAccount, bindUUID) if submitRes != nil && len(submitRes.PatchResultData) > 0 { stage := deliveryStageCreateOrder if v, ok := submitRes.PatchResultData["provider_order_stage"].(string); ok && v != "" { stage = v } _ = s.recordDeliverySubmissionStage(claimed, stage, submitRes.PatchResultData) } if err != nil { failStage := deliveryStageCreateOrder if submitRes != nil { if v, ok := submitRes.PatchResultData["provider_order_stage"].(string); ok && v != "" { failStage = v } } s.markDeliverySubmissionFailed(claimed, apiClientID, failStage, err.Error()) return nil, err } resultData := mergeResultData(claimed.ResultData, submitRes.PatchResultData) updated, err := s.fulfillment.UpdateFulfillment(FulfillmentUpdateInput{ MerchantID: order.MerchantID, APIClientID: apiClientID, OrderNo: order.OrderNo, Status: submitRes.NextStatus, ProviderOrderNo: submitRes.ProviderOrderNo, ResultData: resultData, }) if err != nil { // 上游已有身份时先固化 result_data,避免重试双发 _ = s.recordDeliverySubmissionStage(claimed, deliveryStageSubmitted, submitRes.PatchResultData) return nil, err } return &DeliverySubmitResult{ OrderNo: updated.OrderNo, Status: normalizeOrderStatus(updated), Message: submitRes.Message, ProviderOrderNo: submitRes.ProviderOrderNo, GameAccount: submitRes.GameAccount, UpstreamOrder: submitRes.UpstreamOrder, }, nil } func (s *DeliveryService) claimDeliverySubmission(merchantID, apiClientID uint, orderNo string) (*model.FulfillmentOrder, bool, error) { var updated model.FulfillmentOrder claimedNow := false err := s.fulfillment.db.Transaction(func(tx *gorm.DB) error { var order model.FulfillmentOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Preload("MerchantProduct"). Where("merchant_id = ? AND order_no = ?", merchantID, orderNo). First(&order).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("订单不存在") } return err } orderStatus := normalizeOrderStatus(&order) if orderStatus == model.OrderStatusDelivering || orderStatus == model.OrderStatusDelivered { updated = order return nil } if err := validateOrderStatusTransition(&order, model.OrderStatusDelivering, fulfillmentTransitionUpdate); err != nil { return newDeliveryHTTPError(http.StatusConflict, "订单暂不可发货") } now := time.Now() stage := resultDataString(order.ResultData, "provider_order_stage") startedAtRaw := resultDataString(order.ResultData, "submit_started_at") // 1. 如果已有协程正在活跃提交中(在 35 秒执行窗口内且处于 claimed/queue_created 阶段), // 即使已被外部 Webhook 临时推回 ship_failed,也绝对不允许并发重入发货打架 if deliverySubmissionInProgress(stage) && startedAtRaw != "" { if startedAt, parseErr := time.Parse(timeutil.APITimeLayout, startedAtRaw); parseErr == nil { if now.Sub(startedAt) < 35*time.Second { updated = order return nil } } } // 2. 正常并发防双发:仅对 paid 等非最终失败态订单检查 10 分钟未超时 if orderStatus != model.OrderStatusShipFailed && deliverySubmissionInProgress(stage) && !deliverySubmissionStale(&order, now) { updated = order return nil } patch := map[string]interface{}{ "source": "delivery_proxy", "submit_started_at": timeutil.FormatAPITime(now), "provider_order_stage": deliveryStageClaimed, "ship_attempts": resultDataNumber(order.ResultData, "ship_attempts") + 1, } if err := tx.Model(&order).Updates(map[string]interface{}{ "result_data": mergeResultData(order.ResultData, patch), "failure_reason": "", }).Error; err != nil { return err } claimedNow = true if err := tx.Preload("MerchantProduct").First(&updated, order.ID).Error; err != nil { return err } apiClientIDPtr := optionalUint(apiClientID) if err := writeAudit(tx, &merchantID, nil, apiClientIDPtr, "delivery.submit.claim", "fulfillment_order", order.OrderNo, nil); err != nil { return err } return nil }) if err != nil { return nil, false, err } return &updated, claimedNow, nil } func (s *DeliveryService) recordDeliverySubmissionStage(order *model.FulfillmentOrder, stage string, patch map[string]interface{}) error { if order == nil { return errors.New("订单不存在") } if patch == nil { patch = map[string]interface{}{} } patch["provider_order_stage"] = stage resultData := mergeResultData(order.ResultData, patch) if err := s.fulfillment.db.Model(&model.FulfillmentOrder{}). Where("id = ?", order.ID). Update("result_data", resultData).Error; err != nil { return err } order.ResultData = resultData return nil } func (s *DeliveryService) markDeliverySubmissionFailed(order *model.FulfillmentOrder, apiClientID uint, stage, reason string) { if order == nil { return } latest := order if fresh, err := s.fulfillment.GetOrder(order.MerchantID, order.OrderNo); err == nil { latest = fresh } // 确保失败时 stage 绝不能继续保持在 in_progress(claimed / queue_created),否则阻碍重试 releaseStage := stage if deliverySubmissionInProgress(releaseStage) { releaseStage = deliveryStageCreateOrder } resultData := mergeResultData(latest.ResultData, map[string]interface{}{ "provider_order_stage": releaseStage, "fail_stage": stage, "fail_reason": reason, "fail_at": timeutil.FormatAPITime(time.Now()), }) _, _ = s.fulfillment.UpdateFulfillment(FulfillmentUpdateInput{ MerchantID: order.MerchantID, APIClientID: apiClientID, OrderNo: order.OrderNo, Status: model.OrderStatusShipFailed, FailureReason: reason, ResultData: resultData, }) }