代练仓按原单号回查能不能发货。随机后缀查不到,正式单会失败。已有队列时应续发原单,不能换号。 Co-authored-by: Cursor <cursoragent@cursor.com>
599 lines
18 KiB
Go
599 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"affiliate_dash/internal/model"
|
|
)
|
|
|
|
type bindCacheEntry struct {
|
|
account map[string]interface{}
|
|
err error
|
|
expiresAt time.Time
|
|
}
|
|
|
|
// DLCProvider 实现第一平台(聚鑫源 / dlc)履约逻辑
|
|
type DLCProvider struct {
|
|
bffBaseURL string
|
|
channel string
|
|
httpClient *http.Client
|
|
bindCacheMu sync.RWMutex
|
|
bindCache map[string]bindCacheEntry
|
|
}
|
|
|
|
func NewDLCProvider(bffBaseURL, channel string, httpClient *http.Client) *DLCProvider {
|
|
bffBaseURL = strings.TrimRight(strings.TrimSpace(bffBaseURL), "/")
|
|
if bffBaseURL == "" {
|
|
bffBaseURL = defaultDeliveryBFFBaseURL
|
|
}
|
|
channel = strings.TrimSpace(channel)
|
|
if channel == "" {
|
|
channel = "dlc"
|
|
}
|
|
if httpClient == nil {
|
|
httpClient = &http.Client{Timeout: deliveryUpstreamTimeout}
|
|
}
|
|
return &DLCProvider{
|
|
bffBaseURL: bffBaseURL,
|
|
channel: channel,
|
|
httpClient: httpClient,
|
|
bindCache: make(map[string]bindCacheEntry),
|
|
}
|
|
}
|
|
|
|
func (p *DLCProvider) Name() string {
|
|
return ProviderDLC
|
|
}
|
|
|
|
func (p *DLCProvider) PrepareDelivery(ctx context.Context, order *model.FulfillmentOrder, openOrder *OpenOrderQuery) (*DeliveryOrderState, error) {
|
|
sku := ""
|
|
configStr := ""
|
|
if order != nil {
|
|
sku = order.ProductSKU
|
|
if order.MerchantProduct != nil {
|
|
configStr = order.MerchantProduct.FulfillmentConfig
|
|
}
|
|
}
|
|
cfg := ParseProductRoutingConfig(configStr, sku)
|
|
goodID := cfg.DLCGoodID
|
|
if goodID == "" {
|
|
goodID = deliveryGoodID(p.channel, sku)
|
|
}
|
|
if goodID == "" {
|
|
return &DeliveryOrderState{
|
|
CanShip: false,
|
|
CannotShipReason: "未找到对应的第一平台商品发货配置",
|
|
Provider: ProviderDLC,
|
|
}, nil
|
|
}
|
|
|
|
var good map[string]interface{}
|
|
good, _ = p.goodsDetail(goodID)
|
|
|
|
var deliveryProd *DeliveryProduct
|
|
if openOrder != nil && openOrder.Product != nil {
|
|
deliveryProd = buildDeliveryProduct(openOrder.Product, good)
|
|
}
|
|
|
|
return &DeliveryOrderState{
|
|
CanShip: true,
|
|
Product: deliveryProd,
|
|
Good: good,
|
|
Provider: ProviderDLC,
|
|
}, nil
|
|
}
|
|
|
|
func (p *DLCProvider) Bind(ctx context.Context, order *model.FulfillmentOrder, gameAccount string) (*ProviderBindResult, error) {
|
|
sku := order.ProductSKU
|
|
configStr := ""
|
|
if order.MerchantProduct != nil {
|
|
configStr = order.MerchantProduct.FulfillmentConfig
|
|
}
|
|
cfg := ParseProductRoutingConfig(configStr, sku)
|
|
goodID := cfg.DLCGoodID
|
|
if goodID == "" {
|
|
goodID = deliveryGoodID(p.channel, sku)
|
|
}
|
|
if goodID == "" {
|
|
return nil, errors.New("未找到对应的商品发货配置")
|
|
}
|
|
|
|
var out struct {
|
|
BindUUID string `json:"bind_uuid"`
|
|
URL string `json:"url"`
|
|
}
|
|
if err := p.signProxy("/public/games/bind-account", "POST", map[string]interface{}{
|
|
"game_account": gameAccount,
|
|
"gameAccount": gameAccount,
|
|
"goodId": goodID,
|
|
"good_id": goodID,
|
|
}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
if out.BindUUID == "" || out.URL == "" {
|
|
return nil, errors.New("绑定服务未返回绑定二维码")
|
|
}
|
|
|
|
return &ProviderBindResult{
|
|
BindUUID: out.BindUUID,
|
|
BindURL: out.URL,
|
|
QRURL: qrCodeURL(out.URL),
|
|
Bound: false,
|
|
Provider: ProviderDLC,
|
|
}, nil
|
|
}
|
|
|
|
func (p *DLCProvider) CheckBind(ctx context.Context, order *model.FulfillmentOrder, bindUUID string) (*ProviderBindResult, error) {
|
|
sku := order.ProductSKU
|
|
configStr := ""
|
|
if order.MerchantProduct != nil {
|
|
configStr = order.MerchantProduct.FulfillmentConfig
|
|
}
|
|
cfg := ParseProductRoutingConfig(configStr, sku)
|
|
goodID := cfg.DLCGoodID
|
|
if goodID == "" {
|
|
goodID = deliveryGoodID(p.channel, sku)
|
|
}
|
|
if goodID == "" {
|
|
return nil, errors.New("未找到对应的商品发货配置")
|
|
}
|
|
|
|
account, err := p.accountBound(bindUUID, goodID)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "尚未绑定") || strings.Contains(err.Error(), "未绑定") {
|
|
return &ProviderBindResult{
|
|
BindUUID: bindUUID,
|
|
Bound: false,
|
|
Message: "尚未绑定,请先完成扫码绑定",
|
|
Provider: ProviderDLC,
|
|
}, nil
|
|
}
|
|
if isRateLimitError(err) {
|
|
return &ProviderBindResult{
|
|
BindUUID: bindUUID,
|
|
Bound: false,
|
|
Message: "正在同步绑定状态,请稍候...",
|
|
Provider: ProviderDLC,
|
|
}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
boundAccount := stringFromMap(account, "game_account")
|
|
result := &ProviderBindResult{
|
|
BindUUID: bindUUID,
|
|
Bound: true,
|
|
GameAccount: boundAccount,
|
|
RoleName: stringFromMap(account, "game_account_role_name"),
|
|
GameChannel: gameChannelText(account),
|
|
Message: "绑定成功",
|
|
Provider: ProviderDLC,
|
|
}
|
|
|
|
if expected := requestDataString(order.RequestData, "game_account"); expected != "" {
|
|
result.ExpectedGameAccount = expected
|
|
if boundAccount != expected {
|
|
result.Mismatch = true
|
|
result.Message = "绑定成功,但绑定账号与下单预期账号不一致,请确认"
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (p *DLCProvider) Submit(ctx context.Context, order *model.FulfillmentOrder, gameAccount, bindUUID string) (*ProviderSubmitResult, error) {
|
|
sku := order.ProductSKU
|
|
configStr := ""
|
|
if order.MerchantProduct != nil {
|
|
configStr = order.MerchantProduct.FulfillmentConfig
|
|
}
|
|
cfg := ParseProductRoutingConfig(configStr, sku)
|
|
goodID := cfg.DLCGoodID
|
|
if goodID == "" {
|
|
goodID = deliveryGoodID(p.channel, sku)
|
|
}
|
|
if goodID == "" {
|
|
return nil, errors.New("未找到对应的商品发货配置")
|
|
}
|
|
|
|
boundAccount, err := p.accountBound(bindUUID, goodID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("绑定校验失败: %w", err)
|
|
}
|
|
if boundGameAccount := stringFromMap(boundAccount, "game_account"); boundGameAccount != "" && boundGameAccount != gameAccount {
|
|
return nil, errors.New("玩家编号与绑定的游戏账号不一致,请重新绑定")
|
|
}
|
|
|
|
goodsNo := sku
|
|
goodsName := order.ProductName
|
|
|
|
queueOrderID := resultDataString(order.ResultData, "queue_order_id")
|
|
partial := map[string]interface{}{
|
|
"provider": ProviderDLC,
|
|
}
|
|
// 代练仓下正式单前会按这个单号回查。随机后缀在库里不存在,不能再拿去下单。
|
|
effectiveOrderSN := strings.TrimSpace(order.OrderNo)
|
|
if savedSN := resultDataString(order.ResultData, "order_sn"); savedSN != "" && savedSN != effectiveOrderSN {
|
|
queueOrderID = ""
|
|
}
|
|
|
|
if queueOrderID == "" {
|
|
createdID, sn, err := p.createOrderQueue(goodID, order.OrderNo, goodsNo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("创建发货队列失败: %w", err)
|
|
}
|
|
queueOrderID = createdID
|
|
effectiveOrderSN = sn
|
|
partial["queue_order_id"] = queueOrderID
|
|
partial["order_sn"] = effectiveOrderSN
|
|
partial["provider_order_stage"] = deliveryStageQueueCreated
|
|
} else {
|
|
partial["queue_order_id"] = queueOrderID
|
|
partial["order_sn"] = effectiveOrderSN
|
|
partial["provider_order_stage"] = deliveryStageQueueCreated
|
|
}
|
|
|
|
if err := p.patchOrderQueue(queueOrderID, gameAccount, bindUUID, goodsNo, goodsName); err != nil {
|
|
if isQueueDeadError(err) {
|
|
// 旧队列已失效/不可操作,清除失效队列并重新创建
|
|
delete(partial, "queue_order_id")
|
|
newQueueID, newSN, createErr := p.createOrderQueue(goodID, order.OrderNo, goodsNo)
|
|
if createErr != nil {
|
|
return &ProviderSubmitResult{PatchResultData: partial}, fmt.Errorf("重置发货队列失败: %w (原错误: %v)", createErr, err)
|
|
}
|
|
queueOrderID = newQueueID
|
|
effectiveOrderSN = newSN
|
|
partial["queue_order_id"] = queueOrderID
|
|
partial["order_sn"] = effectiveOrderSN
|
|
if patchErr := p.patchOrderQueue(queueOrderID, gameAccount, bindUUID, goodsNo, goodsName); patchErr != nil {
|
|
return &ProviderSubmitResult{PatchResultData: partial}, fmt.Errorf("补充发货账号失败: %w", patchErr)
|
|
}
|
|
} else {
|
|
return &ProviderSubmitResult{PatchResultData: partial}, fmt.Errorf("补充发货账号失败: %w", err)
|
|
}
|
|
}
|
|
|
|
upstreamOrder, err := p.createUpstreamOrder(queueOrderID, gameAccount, goodID, effectiveOrderSN)
|
|
if err != nil {
|
|
return &ProviderSubmitResult{PatchResultData: partial}, fmt.Errorf("创建上游正式订单失败: %w", err)
|
|
}
|
|
|
|
providerOrderNo := firstNonEmpty(
|
|
stringFromMap(upstreamOrder, "order_no"),
|
|
stringFromMap(upstreamOrder, "_id"),
|
|
stringFromMap(upstreamOrder, "id"),
|
|
stringFromMap(upstreamOrder, "order_sn"),
|
|
queueOrderID,
|
|
)
|
|
|
|
nextStatus := model.OrderStatusDelivering
|
|
message := "已提交发货,等待发货结果回传"
|
|
if upstreamDeliverySucceeded(upstreamOrder) {
|
|
nextStatus = model.OrderStatusDelivered
|
|
message = "发货已完成"
|
|
}
|
|
|
|
return &ProviderSubmitResult{
|
|
ProviderOrderNo: providerOrderNo,
|
|
NextStatus: nextStatus,
|
|
Message: message,
|
|
GameAccount: boundAccount,
|
|
UpstreamOrder: upstreamOrder,
|
|
PatchResultData: map[string]interface{}{
|
|
"provider": ProviderDLC,
|
|
"provider_order_stage": deliveryStageSubmitted,
|
|
"queue_order_id": queueOrderID,
|
|
"provider_order_no": providerOrderNo,
|
|
"game_uid": gameAccount,
|
|
"role_name": stringFromMap(boundAccount, "game_account_role_name"),
|
|
"game_channel": gameChannelText(boundAccount),
|
|
"delivery": upstreamOrder,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (p *DLCProvider) Cancel(ctx context.Context, order *model.FulfillmentOrder) error {
|
|
// DLC 不支持在已提交后自动取消,直接返回 nil
|
|
return nil
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// DLC 内部 HTTP 交互
|
|
// -----------------------------------------------------------------------------
|
|
|
|
func (p *DLCProvider) goodsDetail(goodID string) (map[string]interface{}, error) {
|
|
var out struct {
|
|
Good map[string]interface{} `json:"good"`
|
|
}
|
|
if err := p.signProxy("/public/goods/detail", "POST", map[string]interface{}{
|
|
"good_id": goodID,
|
|
}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out.Good, nil
|
|
}
|
|
|
|
func (p *DLCProvider) accountBound(bindUUID, goodID string) (map[string]interface{}, error) {
|
|
cacheKey := bindUUID + ":" + goodID
|
|
p.bindCacheMu.RLock()
|
|
if entry, ok := p.bindCache[cacheKey]; ok && time.Now().Before(entry.expiresAt) {
|
|
p.bindCacheMu.RUnlock()
|
|
return entry.account, entry.err
|
|
}
|
|
p.bindCacheMu.RUnlock()
|
|
|
|
var out struct {
|
|
GameAccount map[string]interface{} `json:"gameAccount"`
|
|
Snake map[string]interface{} `json:"game_account"`
|
|
}
|
|
err := p.signProxy("/public/games/account-bound", "POST", map[string]interface{}{
|
|
"bind_uuid": bindUUID,
|
|
"bindUuid": bindUUID,
|
|
"goodId": goodID,
|
|
"good_id": goodID,
|
|
}, &out)
|
|
|
|
var account map[string]interface{}
|
|
ttl := 5 * time.Second
|
|
|
|
if err != nil {
|
|
if isRateLimitError(err) {
|
|
ttl = 15 * time.Second
|
|
}
|
|
} else {
|
|
account = out.GameAccount
|
|
if account == nil {
|
|
account = out.Snake
|
|
}
|
|
if account == nil {
|
|
err = errors.New("账号尚未绑定,请扫码完成绑定后再提交")
|
|
ttl = 5 * time.Second
|
|
} else {
|
|
ttl = 30 * time.Second
|
|
}
|
|
}
|
|
|
|
p.bindCacheMu.Lock()
|
|
p.bindCache[cacheKey] = bindCacheEntry{
|
|
account: account,
|
|
err: err,
|
|
expiresAt: time.Now().Add(ttl),
|
|
}
|
|
p.bindCacheMu.Unlock()
|
|
|
|
return account, err
|
|
}
|
|
|
|
func (p *DLCProvider) createOrderQueue(goodID, orderNo, goodsNo string) (string, string, error) {
|
|
var out struct {
|
|
Orders []map[string]interface{} `json:"orders"`
|
|
}
|
|
orderNo = strings.TrimSpace(orderNo)
|
|
payload := map[string]interface{}{
|
|
"good_id": goodID,
|
|
"quantity": 1,
|
|
"order_sn": orderNo,
|
|
"h5_prefix": p.channel,
|
|
}
|
|
if goodsNo != "" {
|
|
payload["goods_no"] = goodsNo
|
|
}
|
|
// 代练仓已有这条单号时,原文是「请续发未完成项或重试失败项」。换一个带后缀的单号会让回查 404。
|
|
if err := p.signProxy("/public/users/orders-queue", "POST", payload, &out); err != nil {
|
|
return "", orderNo, err
|
|
}
|
|
if len(out.Orders) == 0 {
|
|
return "", orderNo, errors.New("发货服务未返回队列订单")
|
|
}
|
|
orderID := firstNonEmpty(stringFromMap(out.Orders[0], "_id"), stringFromMap(out.Orders[0], "id"))
|
|
if orderID == "" {
|
|
return "", orderNo, errors.New("发货服务未返回队列订单 ID")
|
|
}
|
|
return orderID, orderNo, nil
|
|
}
|
|
|
|
func (p *DLCProvider) patchOrderQueue(orderID, gameAccount, bindUUID, goodsNo, goodsName string) error {
|
|
var out map[string]interface{}
|
|
payload := map[string]interface{}{
|
|
"order_id": orderID,
|
|
"game_account": gameAccount,
|
|
"bind_uuid": bindUUID,
|
|
"h5_prefix": p.channel,
|
|
}
|
|
if goodsNo != "" {
|
|
payload["goods_no"] = goodsNo
|
|
}
|
|
if goodsName != "" {
|
|
payload["goods_name"] = goodsName
|
|
}
|
|
return p.signProxy("/public/users/orders-queue", "PATCH", payload, &out)
|
|
}
|
|
|
|
func (p *DLCProvider) createUpstreamOrder(orderID, gameAccount, goodID, orderNo string) (map[string]interface{}, error) {
|
|
var out struct {
|
|
Order map[string]interface{} `json:"order"`
|
|
}
|
|
if err := p.signProxy("/public/users/orders", "POST", map[string]interface{}{
|
|
"order_id": orderID,
|
|
"game_account": gameAccount,
|
|
"good_id": goodID,
|
|
"h5_prefix": p.channel,
|
|
"order_sn": orderNo,
|
|
}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
if out.Order == nil {
|
|
out.Order = map[string]interface{}{"order_id": orderID, "order_sn": orderNo}
|
|
}
|
|
return out.Order, nil
|
|
}
|
|
|
|
func (p *DLCProvider) SyncStatus(ctx context.Context, order *model.FulfillmentOrder) (*ProviderSyncResult, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func isQueueDeadError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
msg := err.Error()
|
|
return strings.Contains(msg, "订单不可操作") ||
|
|
strings.Contains(msg, "不可操作") ||
|
|
strings.Contains(msg, "订单不存在") ||
|
|
strings.Contains(msg, "队列已失效") ||
|
|
strings.Contains(msg, "正在处理中") ||
|
|
strings.Contains(msg, "请勿重复提交") ||
|
|
strings.Contains(msg, "已有发货记录") ||
|
|
strings.Contains(msg, "已处理")
|
|
}
|
|
|
|
func isRateLimitError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
msg := strings.ToLower(err.Error())
|
|
return strings.Contains(msg, "请求过于频繁") ||
|
|
strings.Contains(msg, "稍后再试") ||
|
|
strings.Contains(msg, "too many requests") ||
|
|
strings.Contains(msg, "rate limit")
|
|
}
|
|
|
|
func isTransientDeliveryError(err error, statusCode int, message string) bool {
|
|
if err != nil {
|
|
return true
|
|
}
|
|
// 429 Too Many Requests 明确表示上游频控限流,严禁在毫秒/秒级盲目重试,必须立即降级避让
|
|
if statusCode == http.StatusTooManyRequests {
|
|
return false
|
|
}
|
|
if statusCode == http.StatusBadGateway || statusCode == http.StatusServiceUnavailable || statusCode == http.StatusGatewayTimeout {
|
|
return true
|
|
}
|
|
msg := strings.ToLower(message)
|
|
if strings.Contains(msg, "请求过于频繁") || strings.Contains(msg, "too many requests") || strings.Contains(msg, "rate limit") {
|
|
return false
|
|
}
|
|
return strings.Contains(msg, "系统繁忙") || strings.Contains(msg, "网络繁忙") || strings.Contains(msg, "稍后再试")
|
|
}
|
|
|
|
func generateForwardedIP(seed string) string {
|
|
if seed == "" {
|
|
seed = fmt.Sprintf("%d", time.Now().UnixNano())
|
|
}
|
|
h := fnv.New32a()
|
|
_, _ = h.Write([]byte(seed))
|
|
val := h.Sum32()
|
|
b1 := byte((val >> 16) & 0xFF)
|
|
b2 := byte((val >> 8) & 0xFF)
|
|
if b1 == 0 {
|
|
b1 = 1
|
|
}
|
|
if b2 == 0 {
|
|
b2 = 1
|
|
}
|
|
return fmt.Sprintf("118.248.%d.%d", b1, b2)
|
|
}
|
|
|
|
func (p *DLCProvider) signProxy(path, method string, data interface{}, out interface{}) error {
|
|
payload, err := json.Marshal(map[string]interface{}{
|
|
"path": path,
|
|
"method": strings.ToUpper(method),
|
|
"data": data,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
maxRetries := 2
|
|
if path == "/public/games/account-bound" || path == "/public/users/orders" || path == "/public/games/bind-account" {
|
|
maxRetries = 0 // 轮询绑定接口、正式下单接口与绑定生成接口不触发传输层重试,避免堆积阻塞或单号重复冲突
|
|
}
|
|
var lastErr error
|
|
|
|
for attempt := 0; attempt <= maxRetries; attempt++ {
|
|
if attempt > 0 {
|
|
// 遭遇瞬时网络波动时的指数避让 (1.2s, 2.4s)
|
|
backoff := time.Duration(1200*attempt)*time.Millisecond + time.Duration((attempt*137)%300)*time.Millisecond
|
|
time.Sleep(backoff)
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodPost, p.bffBaseURL+"/sign-proxy", bytes.NewReader(payload))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
seed := ""
|
|
if m, ok := data.(map[string]interface{}); ok {
|
|
seed = firstNonEmpty(
|
|
stringFromMap(m, "order_sn"),
|
|
stringFromMap(m, "bind_uuid"),
|
|
stringFromMap(m, "bindUuid"),
|
|
stringFromMap(m, "game_account"),
|
|
stringFromMap(m, "gameAccount"),
|
|
stringFromMap(m, "good_id"),
|
|
stringFromMap(m, "goodId"),
|
|
)
|
|
}
|
|
forwardedIP := generateForwardedIP(seed)
|
|
req.Header.Set("X-Forwarded-For", forwardedIP)
|
|
req.Header.Set("X-Real-IP", forwardedIP)
|
|
|
|
resp, err := p.httpClient.Do(req)
|
|
if err != nil {
|
|
lastErr = fmt.Errorf("发货服务请求失败:%w", err)
|
|
if attempt < maxRetries && isTransientDeliveryError(err, 0, "") {
|
|
continue
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
resp.Body.Close()
|
|
|
|
var envelope struct {
|
|
Code interface{} `json:"code"`
|
|
Message string `json:"message"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(body, &envelope); err != nil {
|
|
lastErr = fmt.Errorf("发货服务响应无法解析:%s", truncateDeliveryText(string(body)))
|
|
if attempt < maxRetries && resp.StatusCode >= http.StatusInternalServerError {
|
|
continue
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices || !isZeroCode(envelope.Code) {
|
|
errMsg := envelope.Message
|
|
if errMsg == "" {
|
|
errMsg = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
|
}
|
|
lastErr = errors.New(errMsg)
|
|
if attempt < maxRetries && isTransientDeliveryError(nil, resp.StatusCode, errMsg) {
|
|
continue
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
if out == nil || len(envelope.Data) == 0 || string(envelope.Data) == "null" {
|
|
return nil
|
|
}
|
|
if err := json.Unmarshal(envelope.Data, out); err != nil {
|
|
return fmt.Errorf("发货服务数据无法解析:%w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return lastErr
|
|
}
|