补商户发货API

This commit is contained in:
yml2213
2026-07-31 12:24:54 +08:00
parent e4c1306216
commit 761522e808
7 changed files with 580 additions and 8 deletions
+293 -4
View File
@@ -16,6 +16,9 @@ import (
"time"
"affiliate_dash/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const defaultDeliveryBFFBaseURL = "https://www.jxya.top/bff-stg"
@@ -139,15 +142,50 @@ func (s *DeliveryService) GetOrder(orderNo string, auth DeliveryLinkAuth) (*Deli
return info, err
}
func (s *DeliveryService) GetMerchantOrder(merchantID uint, orderNo string) (*DeliveryOrderInfo, error) {
openOrder, order, goodID, err := s.loadMerchantOrder(merchantID, orderNo)
if err != nil {
return nil, err
}
info, _, _, err := s.buildDeliveryState(openOrder, order, goodID, false)
return info, err
}
func (s *DeliveryService) Bind(orderNo, gameAccount string, auth DeliveryLinkAuth) (*DeliveryBindResult, error) {
return s.bindWithOrder(orderNo, gameAccount, &auth, 0)
}
func (s *DeliveryService) BindForMerchant(merchantID uint, orderNo, gameAccount string) (*DeliveryBindResult, error) {
return s.bindWithOrder(orderNo, gameAccount, nil, merchantID)
}
func (s *DeliveryService) bindWithOrder(orderNo, gameAccount string, auth *DeliveryLinkAuth, merchantID uint) (*DeliveryBindResult, error) {
gameAccount = strings.TrimSpace(gameAccount)
if gameAccount == "" {
return nil, errors.New("请输入玩家编号")
}
_, _, goodID, err := s.prepareOrder(orderNo, true, &auth)
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
}
_, _, _, err = s.buildDeliveryState(openOrder, order, goodID, true)
if err != nil {
return nil, err
}
return s.bindGood(gameAccount, goodID)
}
func (s *DeliveryService) bindGood(gameAccount, goodID string) (*DeliveryBindResult, error) {
var out struct {
BindUUID string `json:"bind_uuid"`
URL string `json:"url"`
@@ -171,32 +209,85 @@ func (s *DeliveryService) Bind(orderNo, gameAccount string, auth DeliveryLinkAut
}
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("玩家编号和绑定凭证不能为空")
}
_, order, goodID, err := s.prepareOrder(orderNo, true, &auth)
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
}
if order.FulfillmentStatus == model.FulfillmentStatusProcessing ||
order.FulfillmentStatus == model.FulfillmentStatusSucceeded ||
strings.TrimSpace(order.ProviderOrderNo) != "" ||
strings.Contains(order.ResultData, "\"queue_order_id\"") {
if existing := buildExistingDeliverySubmitResult(order); existing != nil {
return existing, nil
}
}
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, "订单暂时正在履约中,请稍后查询")
}
boundAccount, err := s.accountBound(bindUUID, goodID)
if err != nil {
s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error())
return nil, err
}
if boundGameAccount := stringFromMap(boundAccount, "game_account"); boundGameAccount != "" && boundGameAccount != gameAccount {
return nil, errors.New("玩家编号与绑定的游戏账号不一致,请重新绑定")
err := errors.New("玩家编号与绑定的游戏账号不一致,请重新绑定")
s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error())
return nil, err
}
queueOrderID, err := s.createOrderQueue(goodID, order.OrderNo)
if err != nil {
s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error())
return nil, err
}
if err := s.patchOrderQueue(queueOrderID, gameAccount, bindUUID); err != nil {
s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error())
return nil, err
}
upstreamOrder, err := s.createUpstreamOrder(queueOrderID, gameAccount, goodID, order.OrderNo)
if err != nil {
s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error())
return nil, err
}
providerOrderNo := firstNonEmpty(
@@ -217,7 +308,7 @@ func (s *DeliveryService) Submit(orderNo, gameAccount, bindUUID string, auth Del
}
updated, err := s.fulfillment.UpdateFulfillment(FulfillmentUpdateInput{
MerchantID: order.MerchantID,
APIClientID: 0,
APIClientID: apiClientID,
OrderNo: order.OrderNo,
Status: "processing",
ProviderOrderNo: providerOrderNo,
@@ -236,6 +327,204 @@ func (s *DeliveryService) Submit(orderNo, gameAccount, bindUUID string, auth Del
}, 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"}).
Where("merchant_id = ? AND order_no = ?", merchantID, orderNo).
First(&order).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("订单不存在")
}
return err
}
if order.FulfillmentStatus == model.FulfillmentStatusProcessing || order.FulfillmentStatus == model.FulfillmentStatusSucceeded {
updated = order
return nil
}
if order.FulfillmentStatus != model.FulfillmentStatusPending && order.FulfillmentStatus != model.FulfillmentStatusFailed {
return newDeliveryHTTPError(http.StatusConflict, "订单暂不可发货")
}
raw, _ := json.Marshal(map[string]interface{}{
"source": "delivery_proxy",
"submit_started_at": time.Now().UTC().Format(time.RFC3339),
"provider_order_stage": "claimed",
})
if err := tx.Model(&order).Updates(map[string]interface{}{
"fulfillment_status": model.FulfillmentStatusProcessing,
"result_data": string(raw),
"failure_reason": "",
}).Error; err != nil {
return err
}
claimedNow = true
if err := tx.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
}
if s.fulfillment.callbacks != nil {
if err := s.fulfillment.callbacks.Enqueue(tx, merchantID, "order.fulfillment.updated", orderCallbackData(&updated)); err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, false, err
}
return &updated, claimedNow, nil
}
func (s *DeliveryService) markDeliverySubmissionFailed(order *model.FulfillmentOrder, apiClientID uint, reason string) {
if order == nil {
return
}
_, _ = s.fulfillment.UpdateFulfillment(FulfillmentUpdateInput{
MerchantID: order.MerchantID,
APIClientID: apiClientID,
OrderNo: order.OrderNo,
Status: model.FulfillmentStatusFailed,
FailureReason: reason,
ResultData: map[string]interface{}{
"source": "delivery_proxy",
"fail_reason": reason,
},
})
}
func (s *DeliveryService) loadLinkedOrder(orderNo string, auth *DeliveryLinkAuth) (*OpenOrderQuery, *model.FulfillmentOrder, string, error) {
openOrder, err := s.fulfillment.QueryOpenOrder(orderNo)
if err != nil {
return nil, nil, "", err
}
order, err := s.fulfillment.GetByOrderNo(orderNo)
if err != nil {
return nil, nil, "", err
}
if auth != nil {
if err := s.authorizeDeliveryLink(order, *auth); err != nil {
return nil, nil, "", err
}
}
goodID := ""
if openOrder.Product != nil {
goodID = deliveryGoodID(s.channel, openOrder.Product.SKU)
}
return openOrder, order, goodID, nil
}
func (s *DeliveryService) loadMerchantOrder(merchantID uint, orderNo string) (*OpenOrderQuery, *model.FulfillmentOrder, string, error) {
if merchantID == 0 {
return nil, nil, "", errors.New("无效的商户")
}
openOrder, err := s.fulfillment.QueryOpenOrder(orderNo)
if err != nil {
return nil, nil, "", err
}
order, err := s.fulfillment.GetOrder(merchantID, orderNo)
if err != nil {
return nil, nil, "", err
}
goodID := ""
if openOrder.Product != nil {
goodID = deliveryGoodID(s.channel, openOrder.Product.SKU)
}
return openOrder, order, goodID, nil
}
func (s *DeliveryService) buildDeliveryState(openOrder *OpenOrderQuery, order *model.FulfillmentOrder, goodID string, requireCanShip bool) (*DeliveryOrderInfo, *model.FulfillmentOrder, string, error) {
if openOrder == nil || order == nil {
return nil, nil, "", errors.New("订单不存在")
}
canShip, reason := CanFulfill(order)
if !openOrder.CanShip {
reason = openOrder.CannotShipReason
canShip = false
}
if !canShip && reason == "" {
reason = "订单暂不可发货"
}
if openOrder.Product == nil || openOrder.Product.SKU == "" {
canShip = false
reason = "订单缺少商品 SKU"
} else if goodID == "" {
goodID = deliveryGoodID(s.channel, openOrder.Product.SKU)
if goodID == "" {
canShip = false
reason = "未找到对应的上游商品配置"
}
}
if requireCanShip && !canShip {
return nil, nil, "", newDeliveryHTTPError(http.StatusBadRequest, reason)
}
var good map[string]interface{}
if goodID != "" {
good, _ = s.goodsDetail(goodID)
}
product := buildDeliveryProduct(openOrder.Product, good)
info := &DeliveryOrderInfo{
OrderNo: openOrder.OrderNo,
Status: openOrder.Status,
CanShip: canShip,
CannotShipReason: reason,
Product: product,
BuyerName: openOrder.BuyerName,
Amount: openOrder.Amount,
CreatedAt: openOrder.CreatedAt,
ShippedAt: openOrder.ShippedAt,
ShipFailReason: openOrder.ShipFailReason,
GameChannel: openOrder.GameChannel,
GameUID: openOrder.GameUID,
RoleName: openOrder.RoleName,
PayScore: openOrder.PayScore,
Good: good,
}
return info, order, goodID, nil
}
func buildExistingDeliverySubmitResult(order *model.FulfillmentOrder) *DeliverySubmitResult {
if order == nil {
return nil
}
switch order.FulfillmentStatus {
case model.FulfillmentStatusProcessing, model.FulfillmentStatusSucceeded:
default:
return nil
}
var resultData map[string]interface{}
if json.Valid([]byte(order.ResultData)) {
_ = json.Unmarshal([]byte(order.ResultData), &resultData)
}
providerOrderNo := firstNonEmpty(
order.ProviderOrderNo,
stringFromMap(resultData, "provider_order_no"),
)
message := "订单暂时正在履约中,请稍后查询"
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded {
message = "订单已履约成功"
}
return &DeliverySubmitResult{
OrderNo: order.OrderNo,
Status: order.FulfillmentStatus,
Message: message,
ProviderOrderNo: providerOrderNo,
UpstreamOrder: resultData,
}
}
func optionalUint(value uint) *uint {
if value == 0 {
return nil
}
out := value
return &out
}
func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo string) (*DeliveryLinkResult, error) {
orderNo = strings.TrimSpace(orderNo)
if orderNo == "" {