纯文件级移动,无行为变更: - 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 声明完全一致(无缺失、无多出、无重复)。
249 lines
8.8 KiB
Go
249 lines
8.8 KiB
Go
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,
|
||
})
|
||
}
|