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 声明完全一致(无缺失、无多出、无重复)。
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,207 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// GetBindResult 查询绑定结果(自建发货页轮询用)。
|
||||
// 绑定未完成时返回 bound=false(不报错),由前端提示继续扫码。
|
||||
func (s *DeliveryService) GetBindResult(merchantID uint, orderNo, bindUUID string) (*DeliveryBindResult, error) {
|
||||
bindUUID = strings.TrimSpace(bindUUID)
|
||||
if bindUUID == "" {
|
||||
return nil, errors.New("bind_uuid 不能为空")
|
||||
}
|
||||
openOrder, order, goodID, err := s.loadMerchantOrder(merchantID, orderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, _, _, err := s.buildDeliveryState(openOrder, order, goodID, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providers, _ := s.router.ResolveProviders(order)
|
||||
if len(providers) == 0 {
|
||||
return nil, errors.New("未配置发货渠道")
|
||||
}
|
||||
p := providers[0]
|
||||
res, err := p.CheckBind(context.Background(), order, bindUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DeliveryBindResult{
|
||||
BindUUID: res.BindUUID,
|
||||
BindURL: res.BindURL,
|
||||
QRURL: res.QRURL,
|
||||
Bound: res.Bound,
|
||||
GameAccount: res.GameAccount,
|
||||
RoleName: res.RoleName,
|
||||
GameChannel: res.GameChannel,
|
||||
ExpectedGameAccount: res.ExpectedGameAccount,
|
||||
Mismatch: res.Mismatch,
|
||||
Message: res.Message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetBindResultLinked 为公开发货链接场景提供查询绑定结果的能力。
|
||||
func (s *DeliveryService) GetBindResultLinked(orderNo, bindUUID string, auth DeliveryLinkAuth) (*DeliveryBindResult, error) {
|
||||
bindUUID = strings.TrimSpace(bindUUID)
|
||||
if bindUUID == "" {
|
||||
return nil, errors.New("bind_uuid 不能为空")
|
||||
}
|
||||
openOrder, order, goodID, err := s.loadLinkedOrder(orderNo, &auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, _, _, err := s.buildDeliveryState(openOrder, order, goodID, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providers, _ := s.router.ResolveProviders(order)
|
||||
if len(providers) == 0 {
|
||||
return nil, errors.New("未配置发货渠道")
|
||||
}
|
||||
p := providers[0]
|
||||
res, err := p.CheckBind(context.Background(), order, bindUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DeliveryBindResult{
|
||||
BindUUID: res.BindUUID,
|
||||
BindURL: res.BindURL,
|
||||
QRURL: res.QRURL,
|
||||
Bound: res.Bound,
|
||||
GameAccount: res.GameAccount,
|
||||
RoleName: res.RoleName,
|
||||
GameChannel: res.GameChannel,
|
||||
ExpectedGameAccount: res.ExpectedGameAccount,
|
||||
Mismatch: res.Mismatch,
|
||||
Message: res.Message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) bindWithOrder(orderNo, gameAccount string, auth *DeliveryLinkAuth, merchantID uint) (*DeliveryBindResult, error) {
|
||||
gameAccount = strings.TrimSpace(gameAccount)
|
||||
if gameAccount == "" {
|
||||
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
|
||||
}
|
||||
_, _, _, err = s.buildDeliveryState(openOrder, order, goodID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
providers, _ := s.router.ResolveProviders(order)
|
||||
if len(providers) == 0 {
|
||||
return nil, errors.New("未配置发货渠道")
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
var bindRes *ProviderBindResult
|
||||
var usedProvider FulfillmentProvider
|
||||
tried := map[string]bool{}
|
||||
for _, p := range providers {
|
||||
tried[p.Name()] = true
|
||||
res, err := p.Bind(context.Background(), order, gameAccount)
|
||||
if err == nil && res != nil {
|
||||
bindRes = res
|
||||
usedProvider = p
|
||||
break
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
// Prepare 可能因 GET 成功先锁住乐玩;随后写接口凭据/IP 校验失败时,清除锁定并尝试其余渠道(如 DLC)。
|
||||
if bindRes == nil {
|
||||
lockedName := resultDataString(order.ResultData, "provider")
|
||||
if lockedName != "" || lastErr != nil {
|
||||
s.clearSelectedProvider(order)
|
||||
fallbackProviders, _ := s.router.ResolveProviders(order)
|
||||
for _, p := range fallbackProviders {
|
||||
if tried[p.Name()] {
|
||||
continue
|
||||
}
|
||||
tried[p.Name()] = true
|
||||
res, err := p.Bind(context.Background(), order, gameAccount)
|
||||
if err == nil && res != nil {
|
||||
bindRes = res
|
||||
usedProvider = p
|
||||
break
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bindRes == nil {
|
||||
if lastErr != nil {
|
||||
return nil, lastErr
|
||||
}
|
||||
return nil, errors.New("发货渠道未就绪")
|
||||
}
|
||||
|
||||
// 记录选定渠道与上游单号(如 lewan_order_id),不提前写入 submit claimed stage
|
||||
if usedProvider != nil {
|
||||
patch := map[string]interface{}{
|
||||
"provider": usedProvider.Name(),
|
||||
}
|
||||
for k, v := range bindRes.PatchResultData {
|
||||
patch[k] = v
|
||||
}
|
||||
_ = s.persistOrderResultPatch(order, patch)
|
||||
}
|
||||
|
||||
return &DeliveryBindResult{
|
||||
BindUUID: bindRes.BindUUID,
|
||||
BindURL: bindRes.BindURL,
|
||||
QRURL: bindRes.QRURL,
|
||||
Bound: bindRes.Bound,
|
||||
GameAccount: bindRes.GameAccount,
|
||||
RoleName: bindRes.RoleName,
|
||||
GameChannel: bindRes.GameChannel,
|
||||
ExpectedGameAccount: bindRes.ExpectedGameAccount,
|
||||
Mismatch: bindRes.Mismatch,
|
||||
Message: bindRes.Message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) bindGood(gameAccount, goodID string) (*DeliveryBindResult, error) {
|
||||
dlc, ok := s.router.GetProvider(ProviderDLC).(*DLCProvider)
|
||||
if !ok || dlc == nil {
|
||||
return nil, errors.New("DLC 渠道未就绪")
|
||||
}
|
||||
res, err := dlc.Bind(context.Background(), &model.FulfillmentOrder{
|
||||
ProductSKU: "",
|
||||
}, gameAccount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DeliveryBindResult{
|
||||
BindUUID: res.BindUUID,
|
||||
BindURL: res.BindURL,
|
||||
QRURL: res.QRURL,
|
||||
Bound: res.Bound,
|
||||
}, nil
|
||||
}
|
||||
@@ -75,3 +75,44 @@ func truncateDeliveryText(value string) string {
|
||||
}
|
||||
return value[:300]
|
||||
}
|
||||
|
||||
func deliveryGoodID(channel, sku string) string {
|
||||
if channel != "dlc" {
|
||||
return ""
|
||||
}
|
||||
return map[string]string{
|
||||
"suit_alan_walker": "682ef3c5a8f40c4234c59f47",
|
||||
"suit_shadow_gothic": "682ef39ca8f40c4234c59f36",
|
||||
"top_black_elite_trainer": "682ef39ca8f40c4234c59f37",
|
||||
"m416_hamster_gray": "682ef3c5a8f40c4234c59f52",
|
||||
"bag_cute_bear": "682ef3c5a8f40c4234c59f4a",
|
||||
"suit_dual_fluffy": "682ef3c5a8f40c4234c59f49",
|
||||
"suit_pink_sheep": "682ef3c5a8f40c4234c59f4d",
|
||||
"suit_first_peach": "682ef3c5a8f40c4234c59f48",
|
||||
"suit_romantic_destiny": "682ef39ca8f40c4234c59f38",
|
||||
"pack_western_cowboy": "682ef3c5a8f40c4234c59f4c",
|
||||
"smoke_pink_sheep": "682ef3c5a8f40c4234c59f50",
|
||||
"frag_pink_sheep": "682ef3c5a8f40c4234c59f51",
|
||||
"suit_hamster_gray": "682ef3c5a8f40c4234c59f53",
|
||||
"suit_cute_bear": "682ef3c5a8f40c4234c59f4b",
|
||||
"bag_pink_sheep": "682ef3c5a8f40c4234c59f4f",
|
||||
"helmet_pink_sheep": "682ef3c5a8f40c4234c59f4e",
|
||||
"bag_hamster_gray": "682ef3c5a8f40c4234c59f55",
|
||||
"helmet_hamster_gray": "682ef3c5a8f40c4234c59f54",
|
||||
"suit_western_mystery": "682ef39ca8f40c4234c59f35",
|
||||
"helmet_panda_treasure": "682ef39ca8f40c4234c59f39",
|
||||
"suit_panda_round": "682ef39ca8f40c4234c59f3a",
|
||||
"suit_panda_tuan": "682ef39ca8f40c4234c59f3b",
|
||||
"pack_lava_ranger": "682ef39ca8f40c4234c59f3c",
|
||||
"suit_sand_dancer": "682ef39ca8f40c4234c59f3d",
|
||||
"pack_star_roam_outfit": "682ef39ca8f40c4234c59f3e",
|
||||
"pack_star_roam_weapon": "682ef39ca8f40c4234c59f3f",
|
||||
"suit_cloud_bear": "682ef39ca8f40c4234c59f40",
|
||||
"honor_medal_x2": "682ef39ca8f40c4234c59f45",
|
||||
"honor_medal_x30": "682ef39ca8f40c4234c59f43",
|
||||
"honor_medal_x90": "682ef39ca8f40c4234c59f41",
|
||||
"lucky_coin_x2": "682ef39ca8f40c4234c59f46",
|
||||
"lucky_coin_x30": "682ef39ca8f40c4234c59f44",
|
||||
"lucky_coin_x90": "682ef39ca8f40c4234c59f42",
|
||||
}[sku]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -12,6 +13,8 @@ import (
|
||||
"time"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
"affiliate_dash/internal/pkg/timeutil"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DeliveryService) authorizeDeliveryLink(order *model.FulfillmentOrder, auth DeliveryLinkAuth) error {
|
||||
@@ -88,3 +91,128 @@ func (s *DeliveryService) signDeliveryLink(orderNo string, exp int64) string {
|
||||
_, _ = mac.Write([]byte(orderNo + "|" + strconv.FormatInt(exp, 10)))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo, requestBaseURL string) (*DeliveryLinkResult, error) {
|
||||
orderNo = strings.TrimSpace(orderNo)
|
||||
if orderNo == "" {
|
||||
return nil, errors.New("订单号不能为空")
|
||||
}
|
||||
order, err := s.fulfillment.GetOrder(merchantID, orderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 已取消订单即使存在未过期链接也不再发放(买家侧授权同样拦截已取消订单)。
|
||||
if normalizeOrderStatus(order) == model.OrderStatusCancelled {
|
||||
return nil, newDeliveryHTTPError(http.StatusBadRequest, "订单已取消,无法获取发货链接")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if order.DeliveryLinkRevokedAt != nil {
|
||||
return nil, newDeliveryHTTPError(http.StatusForbidden, "发货链接已作废")
|
||||
}
|
||||
// 复用未过期链接:已交付/发货中订单也允许查看兑换结果或进度,仅停止发放新链接。
|
||||
if order.DeliveryLinkExpiresAt != nil && order.DeliveryLinkExpiresAt.After(now) {
|
||||
expiresAt := order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)
|
||||
return s.buildDeliveryLinkResult(order.OrderNo, &expiresAt, requestBaseURL), nil
|
||||
}
|
||||
// 永久有效链接(已生成、未作废、无有效期)直接复用,不重复生成。
|
||||
if order.DeliveryLinkGeneratedAt != nil && order.DeliveryLinkExpiresAt == nil {
|
||||
return s.buildDeliveryLinkResult(order.OrderNo, nil, requestBaseURL), nil
|
||||
}
|
||||
// 仅待发货(含可重试的发货失败)订单能生成或续期新链接。
|
||||
canShip, reason := CanFulfill(order)
|
||||
if !canShip {
|
||||
if reason == "" {
|
||||
reason = "订单暂不可发货"
|
||||
}
|
||||
return nil, newDeliveryHTTPError(http.StatusBadRequest, reason)
|
||||
}
|
||||
var expiresAt *time.Time
|
||||
if s.linkTTL > 0 {
|
||||
value := now.Add(s.linkTTL).UTC().Truncate(time.Second)
|
||||
expiresAt = &value
|
||||
}
|
||||
if err := s.fulfillment.db.Model(&model.FulfillmentOrder{}).
|
||||
Where("id = ?", order.ID).
|
||||
Updates(map[string]interface{}{
|
||||
"delivery_link_expires_at": expiresAt,
|
||||
"delivery_link_revoked_at": nil,
|
||||
"delivery_link_generated_at": now,
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.buildDeliveryLinkResult(order.OrderNo, expiresAt, requestBaseURL), nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) RevokeDeliveryLink(merchantID, actorUserID uint, orderNo string) error {
|
||||
orderNo = strings.TrimSpace(orderNo)
|
||||
if orderNo == "" {
|
||||
return errors.New("订单号不能为空")
|
||||
}
|
||||
order, err := s.fulfillment.GetOrder(merchantID, orderNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
return s.fulfillment.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.FulfillmentOrder{}).
|
||||
Where("id = ?", order.ID).
|
||||
Updates(map[string]interface{}{
|
||||
"delivery_link_revoked_at": now,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return writeAudit(tx, &merchantID, optionalUint(actorUserID), nil, "delivery_link.revoke", "fulfillment_order", order.OrderNo, map[string]interface{}{
|
||||
"revoked_at": timeutil.FormatAPITime(now),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DeliveryService) RestoreDeliveryLink(merchantID, actorUserID uint, orderNo, requestBaseURL string) (*DeliveryLinkResult, error) {
|
||||
orderNo = strings.TrimSpace(orderNo)
|
||||
if orderNo == "" {
|
||||
return nil, errors.New("订单号不能为空")
|
||||
}
|
||||
order, err := s.fulfillment.GetOrder(merchantID, orderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
canShip, reason := CanFulfill(order)
|
||||
if !canShip {
|
||||
if reason == "" {
|
||||
reason = "订单暂不可发货"
|
||||
}
|
||||
return nil, newDeliveryHTTPError(http.StatusBadRequest, reason)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
var expiresAt *time.Time
|
||||
if s.linkTTL > 0 {
|
||||
value := now.Add(s.linkTTL).Truncate(time.Second)
|
||||
if order.DeliveryLinkExpiresAt != nil && value.Equal(order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)) {
|
||||
value = value.Add(time.Second)
|
||||
}
|
||||
expiresAt = &value
|
||||
}
|
||||
if err := s.fulfillment.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.FulfillmentOrder{}).
|
||||
Where("id = ?", order.ID).
|
||||
Updates(map[string]interface{}{
|
||||
"delivery_link_expires_at": expiresAt,
|
||||
"delivery_link_revoked_at": nil,
|
||||
"delivery_link_generated_at": now,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
metadata := map[string]interface{}{
|
||||
"generated_at": timeutil.FormatAPITime(now),
|
||||
}
|
||||
if expiresAt != nil {
|
||||
metadata["expires_at"] = timeutil.FormatAPITime(*expiresAt)
|
||||
} else {
|
||||
metadata["permanent"] = true
|
||||
}
|
||||
return writeAudit(tx, &merchantID, optionalUint(actorUserID), nil, "delivery_link.restore", "fulfillment_order", order.OrderNo, metadata)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.buildDeliveryLinkResult(order.OrderNo, expiresAt, requestBaseURL), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
)
|
||||
|
||||
func (s *DeliveryService) CancelUpstream(order *model.FulfillmentOrder) error {
|
||||
if order == nil || s.router == nil {
|
||||
return nil
|
||||
}
|
||||
if name := resultDataString(order.ResultData, "provider"); name != "" {
|
||||
if p := s.router.GetProvider(name); p != nil {
|
||||
return p.Cancel(context.Background(), order)
|
||||
}
|
||||
}
|
||||
if resultDataString(order.ResultData, "lewan_order_id") != "" {
|
||||
if p := s.router.GetProvider(ProviderLewan); p != nil {
|
||||
return p.Cancel(context.Background(), order)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncDeliveringOrder 主动向上游渠道查询处于 delivering 状态的订单结果,以补充 Webhook 丢失或延迟。
|
||||
func (s *DeliveryService) SyncDeliveringOrder(ctx context.Context, order *model.FulfillmentOrder) (bool, error) {
|
||||
if order == nil || s.router == nil || order.OrderStatus != model.OrderStatusDelivering {
|
||||
return false, nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
providerName := resultDataString(order.ResultData, "provider")
|
||||
if providerName == "" && (resultDataString(order.ResultData, "lewan_order_id") != "" || resultDataString(order.ResultData, "lewan_order_no") != "" || strings.HasPrefix(order.ProviderOrderNo, "OMU")) {
|
||||
providerName = ProviderLewan
|
||||
}
|
||||
|
||||
providers := make([]FulfillmentProvider, 0, 2)
|
||||
if providerName != "" {
|
||||
if p := s.router.GetProvider(providerName); p != nil {
|
||||
providers = append(providers, p)
|
||||
}
|
||||
}
|
||||
if len(providers) == 0 {
|
||||
resolved, _ := s.router.ResolveProviders(order)
|
||||
providers = resolved
|
||||
}
|
||||
|
||||
for _, p := range providers {
|
||||
res, err := p.SyncStatus(ctx, order)
|
||||
if err != nil || res == nil {
|
||||
continue
|
||||
}
|
||||
if res.Status == "success" || res.Status == "failed" {
|
||||
input := ShipNotifyInput{
|
||||
OrderNo: order.OrderNo,
|
||||
ShipStatus: res.Status,
|
||||
ProviderOrderNo: res.ProviderOrderNo,
|
||||
FailReason: res.FailReason,
|
||||
RawPayload: res.RawPayload,
|
||||
}
|
||||
if res.GameUID != "" {
|
||||
input.GameUID = &res.GameUID
|
||||
}
|
||||
if res.RoleName != "" {
|
||||
input.RoleName = &res.RoleName
|
||||
}
|
||||
if res.RoleArea != "" {
|
||||
input.GameChannel = &res.RoleArea
|
||||
}
|
||||
if _, err := s.fulfillment.HandleShipNotify(input); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
// 如果仍在发货中,且获得了游戏UID/角色名/区服,补充写入 ResultData
|
||||
patch := map[string]interface{}{}
|
||||
if res.GameUID != "" && resultDataString(order.ResultData, "game_uid") == "" {
|
||||
patch["game_uid"] = res.GameUID
|
||||
}
|
||||
if res.RoleName != "" && resultDataString(order.ResultData, "role_name") == "" {
|
||||
patch["role_name"] = res.RoleName
|
||||
}
|
||||
if res.RoleArea != "" && resultDataString(order.ResultData, "game_channel") == "" {
|
||||
patch["game_channel"] = res.RoleArea
|
||||
}
|
||||
if len(patch) > 0 {
|
||||
_ = s.persistOrderResultPatch(order, patch)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// persistOrderResultPatch 合并写入订单 result_data,不改变发货 stage。
|
||||
func (s *DeliveryService) persistOrderResultPatch(order *model.FulfillmentOrder, patch map[string]interface{}) error {
|
||||
if order == nil || len(patch) == 0 {
|
||||
return nil
|
||||
}
|
||||
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) clearSelectedProvider(order *model.FulfillmentOrder) {
|
||||
if order == nil || strings.TrimSpace(order.ResultData) == "" {
|
||||
return
|
||||
}
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(order.ResultData), &raw); err != nil {
|
||||
return
|
||||
}
|
||||
if _, ok := raw["provider"]; !ok {
|
||||
return
|
||||
}
|
||||
delete(raw, "provider")
|
||||
b, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resultData := string(b)
|
||||
_ = s.fulfillment.db.Model(&model.FulfillmentOrder{}).
|
||||
Where("id = ?", order.ID).
|
||||
Update("result_data", resultData).Error
|
||||
order.ResultData = resultData
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
)
|
||||
|
||||
func (s *DeliveryService) GetOrder(orderNo string, auth DeliveryLinkAuth) (*DeliveryOrderInfo, error) {
|
||||
info, _, _, err := s.prepareOrder(orderNo, false, &auth)
|
||||
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) 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
|
||||
}
|
||||
if order.OrderStatus == model.OrderStatusDelivering {
|
||||
if synced, _ := s.SyncDeliveringOrder(context.Background(), order); synced {
|
||||
openOrder, _ = s.fulfillment.QueryOpenOrder(orderNo)
|
||||
order, _ = s.fulfillment.GetOrder(merchantID, orderNo)
|
||||
}
|
||||
}
|
||||
goodID := ""
|
||||
if openOrder != nil && 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"
|
||||
}
|
||||
|
||||
var (
|
||||
deliveryProd *DeliveryProduct
|
||||
goodData map[string]interface{}
|
||||
)
|
||||
|
||||
providers, _ := s.router.ResolveProviders(order)
|
||||
if len(providers) > 0 {
|
||||
locked := resultDataString(order.ResultData, "provider") != ""
|
||||
matchedState, lastReason := s.tryPrepareProviders(order, openOrder, providers)
|
||||
// 已锁定渠道不可发时,清除锁定并按完整优先级重新选择
|
||||
if matchedState == nil && locked {
|
||||
s.clearSelectedProvider(order)
|
||||
providers, _ = s.router.ResolveProviders(order)
|
||||
matchedState, lastReason = s.tryPrepareProviders(order, openOrder, providers)
|
||||
}
|
||||
if matchedState != nil {
|
||||
deliveryProd = matchedState.Product
|
||||
goodData = matchedState.Good
|
||||
if matchedState.Provider != "" {
|
||||
_ = s.persistOrderResultPatch(order, map[string]interface{}{
|
||||
"provider": matchedState.Provider,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
canShip = false
|
||||
if reason == "" || reason == "订单暂不可发货" {
|
||||
reason = lastReason
|
||||
if reason == "" {
|
||||
reason = "未找到适用的履约渠道配置"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if goodID != "" {
|
||||
goodData, _ = s.goodsDetail(goodID)
|
||||
}
|
||||
|
||||
if deliveryProd == nil && openOrder.Product != nil {
|
||||
deliveryProd = buildDeliveryProduct(openOrder.Product, goodData)
|
||||
}
|
||||
|
||||
if requireCanShip && !canShip {
|
||||
return nil, nil, "", newDeliveryHTTPError(http.StatusBadRequest, reason)
|
||||
}
|
||||
|
||||
providerOrderNo := strings.TrimSpace(openOrder.ProviderOrderNo)
|
||||
if providerOrderNo == "" {
|
||||
providerOrderNo = strings.TrimSpace(order.ProviderOrderNo)
|
||||
}
|
||||
if providerOrderNo == "" {
|
||||
providerOrderNo = resultDataString(order.ResultData, "lewan_order_no")
|
||||
}
|
||||
if providerOrderNo == "" {
|
||||
providerOrderNo = resultDataString(order.ResultData, "provider_order_no")
|
||||
}
|
||||
|
||||
providerName := resultDataString(order.ResultData, "provider")
|
||||
if providerName == "" {
|
||||
if strings.TrimSpace(order.ProviderOrderNo) != "" || resultDataString(order.ResultData, "lewan_order_id") != "" {
|
||||
providerName = "lewan"
|
||||
}
|
||||
}
|
||||
|
||||
roleName := openOrder.RoleName
|
||||
if roleName == "" {
|
||||
roleName = resultDataString(order.ResultData, "role_name")
|
||||
}
|
||||
gameChannel := openOrder.GameChannel
|
||||
if gameChannel == "" {
|
||||
gameChannel = resultDataString(order.ResultData, "game_channel")
|
||||
}
|
||||
// 若角色名或区服为空,尝试从关联的 rock_tokens 表反查并自动回填
|
||||
if (roleName == "" || gameChannel == "") && s.fulfillment != nil && s.fulfillment.db != nil {
|
||||
var rockToken model.RockToken
|
||||
if err := s.fulfillment.db.Where("assigned_order_no = ?", order.OrderNo).First(&rockToken).Error; err == nil {
|
||||
patches := map[string]interface{}{}
|
||||
if roleName == "" && rockToken.ActualRoleName != "" {
|
||||
roleName = rockToken.ActualRoleName
|
||||
patches["role_name"] = roleName
|
||||
}
|
||||
if gameChannel == "" && rockToken.ActualChannel != "" {
|
||||
gameChannel = rockToken.ActualChannel
|
||||
patches["game_channel"] = gameChannel
|
||||
}
|
||||
if len(patches) > 0 {
|
||||
_ = s.persistOrderResultPatch(order, patches)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info := &DeliveryOrderInfo{
|
||||
OrderNo: openOrder.OrderNo,
|
||||
Status: openOrder.Status,
|
||||
CanShip: canShip,
|
||||
CannotShipReason: reason,
|
||||
Product: deliveryProd,
|
||||
BuyerName: openOrder.BuyerName,
|
||||
Amount: openOrder.Amount,
|
||||
ProviderOrderNo: providerOrderNo,
|
||||
ProviderName: providerName,
|
||||
CreatedAt: openOrder.CreatedAt,
|
||||
ShippedAt: openOrder.ShippedAt,
|
||||
ShipFailReason: openOrder.ShipFailReason,
|
||||
GameChannel: gameChannel,
|
||||
GameUID: openOrder.GameUID,
|
||||
RoleName: roleName,
|
||||
PayScore: openOrder.PayScore,
|
||||
Data: requestDataMap(order.RequestData),
|
||||
Good: goodData,
|
||||
}
|
||||
return info, order, goodID, nil
|
||||
}
|
||||
|
||||
// tryPrepareProviders 按优先级尝试 Prepare,返回第一个 CanShip 的渠道状态。
|
||||
func (s *DeliveryService) tryPrepareProviders(order *model.FulfillmentOrder, openOrder *OpenOrderQuery, providers []FulfillmentProvider) (*DeliveryOrderState, string) {
|
||||
lastReason := ""
|
||||
for _, p := range providers {
|
||||
state, err := p.PrepareDelivery(context.Background(), order, openOrder)
|
||||
if err != nil || state == nil {
|
||||
continue
|
||||
}
|
||||
if state.Provider == "" {
|
||||
state.Provider = p.Name()
|
||||
}
|
||||
if state.CanShip {
|
||||
return state, ""
|
||||
}
|
||||
if state.CannotShipReason != "" {
|
||||
lastReason = state.CannotShipReason
|
||||
}
|
||||
}
|
||||
return nil, lastReason
|
||||
}
|
||||
|
||||
func (s *DeliveryService) prepareOrder(orderNo string, requireCanShip bool, auth *DeliveryLinkAuth) (*DeliveryOrderInfo, *model.FulfillmentOrder, string, error) {
|
||||
orderNo = strings.TrimSpace(orderNo)
|
||||
if orderNo == "" {
|
||||
return nil, nil, "", errors.New("订单号不能为空")
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
if order.OrderStatus == model.OrderStatusDelivering {
|
||||
if synced, _ := s.SyncDeliveringOrder(context.Background(), order); synced {
|
||||
openOrder, _ = s.fulfillment.QueryOpenOrder(orderNo)
|
||||
order, _ = s.fulfillment.GetByOrderNo(orderNo)
|
||||
}
|
||||
}
|
||||
return s.buildDeliveryState(openOrder, order, "", requireCanShip)
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type DeliveryOrderInfo struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Status string `json:"status"`
|
||||
CanShip bool `json:"can_ship"`
|
||||
CannotShipReason string `json:"cannot_ship_reason,omitempty"`
|
||||
Product *DeliveryProduct `json:"product,omitempty"`
|
||||
BuyerName string `json:"buyer_name"`
|
||||
Amount int64 `json:"amount"`
|
||||
ProviderOrderNo string `json:"provider_order_no,omitempty"`
|
||||
ProviderName string `json:"provider_name,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ShippedAt *time.Time `json:"shipped_at"`
|
||||
ShipFailReason string `json:"ship_fail_reason,omitempty"`
|
||||
GameChannel string `json:"game_channel,omitempty"`
|
||||
GameUID string `json:"game_uid,omitempty"`
|
||||
RoleName string `json:"role_name,omitempty"`
|
||||
PayScore int `json:"pay_score,omitempty"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
Good map[string]interface{} `json:"good,omitempty"`
|
||||
}
|
||||
|
||||
type DeliveryProduct struct {
|
||||
Name string `json:"name"`
|
||||
SKU string `json:"sku"`
|
||||
Game string `json:"game"`
|
||||
Image string `json:"image,omitempty"`
|
||||
}
|
||||
|
||||
type DeliveryBindResult struct {
|
||||
BindUUID string `json:"bind_uuid"`
|
||||
BindURL string `json:"bind_url"`
|
||||
QRURL string `json:"qr_url"`
|
||||
Bound bool `json:"bound"`
|
||||
GameAccount string `json:"game_account,omitempty"`
|
||||
RoleName string `json:"role_name,omitempty"`
|
||||
GameChannel string `json:"game_channel,omitempty"`
|
||||
ExpectedGameAccount string `json:"expected_game_account,omitempty"`
|
||||
Mismatch bool `json:"mismatch,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type DeliveryLinkResult struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
DeliveryURL string `json:"delivery_url"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"` // null 表示永久有效
|
||||
Exp int64 `json:"exp"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
type DeliverySubmitResult struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
ProviderOrderNo string `json:"provider_order_no,omitempty"`
|
||||
GameAccount map[string]interface{} `json:"game_account,omitempty"`
|
||||
UpstreamOrder interface{} `json:"delivery,omitempty"`
|
||||
}
|
||||
|
||||
type DeliveryLinkAuth struct {
|
||||
Exp int64
|
||||
Sign string
|
||||
}
|
||||
|
||||
type deliveryHTTPError struct {
|
||||
status int
|
||||
message string
|
||||
}
|
||||
|
||||
func (e deliveryHTTPError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (e deliveryHTTPError) HTTPStatus() int {
|
||||
return e.status
|
||||
}
|
||||
|
||||
func newDeliveryHTTPError(status int, message string) error {
|
||||
return deliveryHTTPError{status: status, message: message}
|
||||
}
|
||||
Reference in New Issue
Block a user