Files
affiliate_dash/backend/internal/service/delivery_submit.go
T
yml2213 24fa6a97a0 refactor(delivery): 按职责拆分 delivery.go(1162 → 最大 249 行)
纯文件级移动,无行为变更:
- delivery.go            常量、DeliveryService 结构、构造与依赖注入
- delivery_types.go      对外 DTO 与 deliveryHTTPError
- delivery_provider.go   渠道取消/同步与 result_data 维护
- delivery_query.go      订单查询与发货状态构建
- delivery_bind.go       绑定与绑定结果查询
- delivery_submit.go     提交发货与提交阶段状态机
- deliveryGoodID 并入 delivery_helpers.go,链接管理并入 delivery_link.go

校验:拆分前后 55 个 func/type/const 声明完全一致(无缺失、无多出、无重复)。
2026-10-06 19:43:05 +08:00

249 lines
8.8 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 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,
})
}