fix(dlc): 发货重试不再改写订单号
代练仓按原单号回查能不能发货。随机后缀查不到,正式单会失败。已有队列时应续发原单,不能换号。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -220,9 +220,10 @@ func (p *DLCProvider) Submit(ctx context.Context, order *model.FulfillmentOrder,
|
||||
partial := map[string]interface{}{
|
||||
"provider": ProviderDLC,
|
||||
}
|
||||
effectiveOrderSN := order.OrderNo
|
||||
if savedSN := resultDataString(order.ResultData, "order_sn"); savedSN != "" {
|
||||
effectiveOrderSN = savedSN
|
||||
// 代练仓下正式单前会按这个单号回查。随机后缀在库里不存在,不能再拿去下单。
|
||||
effectiveOrderSN := strings.TrimSpace(order.OrderNo)
|
||||
if savedSN := resultDataString(order.ResultData, "order_sn"); savedSN != "" && savedSN != effectiveOrderSN {
|
||||
queueOrderID = ""
|
||||
}
|
||||
|
||||
if queueOrderID == "" {
|
||||
@@ -263,29 +264,7 @@ func (p *DLCProvider) Submit(ctx context.Context, order *model.FulfillmentOrder,
|
||||
|
||||
upstreamOrder, err := p.createUpstreamOrder(queueOrderID, gameAccount, goodID, effectiveOrderSN)
|
||||
if err != nil {
|
||||
if isQueueDeadError(err) {
|
||||
// 创建正式单阶段出现订单不可操作或重复提交,尝试换新重试单号重建队列重试一次
|
||||
delete(partial, "queue_order_id")
|
||||
retryBaseSN := fmt.Sprintf("%s_%x", strings.SplitN(order.OrderNo, "_", 2)[0], time.Now().UnixNano()%0xffff)
|
||||
newQueueID, newSN, createErr := p.createOrderQueue(goodID, retryBaseSN, goodsNo)
|
||||
if createErr == nil {
|
||||
queueOrderID = newQueueID
|
||||
effectiveOrderSN = newSN
|
||||
partial["queue_order_id"] = queueOrderID
|
||||
partial["order_sn"] = effectiveOrderSN
|
||||
if patchErr := p.patchOrderQueue(queueOrderID, gameAccount, bindUUID, goodsNo, goodsName); patchErr == nil {
|
||||
if retriedOrder, retriedErr := p.createUpstreamOrder(queueOrderID, gameAccount, goodID, effectiveOrderSN); retriedErr == nil {
|
||||
upstreamOrder = retriedOrder
|
||||
err = nil
|
||||
} else {
|
||||
err = retriedErr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return &ProviderSubmitResult{PatchResultData: partial}, fmt.Errorf("创建上游正式订单失败: %w", err)
|
||||
}
|
||||
return &ProviderSubmitResult{PatchResultData: partial}, fmt.Errorf("创建上游正式订单失败: %w", err)
|
||||
}
|
||||
|
||||
providerOrderNo := firstNonEmpty(
|
||||
@@ -398,43 +377,28 @@ func (p *DLCProvider) createOrderQueue(goodID, orderNo, goodsNo string) (string,
|
||||
var out struct {
|
||||
Orders []map[string]interface{} `json:"orders"`
|
||||
}
|
||||
usedSN := orderNo
|
||||
orderNo = strings.TrimSpace(orderNo)
|
||||
payload := map[string]interface{}{
|
||||
"good_id": goodID,
|
||||
"quantity": 1,
|
||||
"order_sn": usedSN,
|
||||
"order_sn": orderNo,
|
||||
"h5_prefix": p.channel,
|
||||
}
|
||||
if goodsNo != "" {
|
||||
payload["goods_no"] = goodsNo
|
||||
}
|
||||
err := p.signProxy("/public/users/orders-queue", "POST", payload, &out)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "正在处理中") ||
|
||||
strings.Contains(err.Error(), "请勿重复提交") ||
|
||||
strings.Contains(err.Error(), "已有发货记录") ||
|
||||
strings.Contains(err.Error(), "已处理") {
|
||||
// DLC 已存在此 order_sn 的队列/发货记录,追加随机后缀重新创建队列
|
||||
retrySN := fmt.Sprintf("%s_%x", strings.SplitN(orderNo, "_", 2)[0], time.Now().UnixNano()%0xffff)
|
||||
usedSN = retrySN
|
||||
payload["order_sn"] = usedSN
|
||||
out.Orders = nil
|
||||
if retryErr := p.signProxy("/public/users/orders-queue", "POST", payload, &out); retryErr == nil && len(out.Orders) > 0 {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
// 代练仓已有这条单号时,原文是「请续发未完成项或重试失败项」。换一个带后缀的单号会让回查 404。
|
||||
if err := p.signProxy("/public/users/orders-queue", "POST", payload, &out); err != nil {
|
||||
return "", orderNo, err
|
||||
}
|
||||
if len(out.Orders) == 0 {
|
||||
return "", "", errors.New("发货服务未返回队列订单")
|
||||
return "", orderNo, errors.New("发货服务未返回队列订单")
|
||||
}
|
||||
orderID := firstNonEmpty(stringFromMap(out.Orders[0], "_id"), stringFromMap(out.Orders[0], "id"))
|
||||
if orderID == "" {
|
||||
return "", "", errors.New("发货服务未返回队列订单 ID")
|
||||
return "", orderNo, errors.New("发货服务未返回队列订单 ID")
|
||||
}
|
||||
return orderID, usedSN, nil
|
||||
return orderID, orderNo, nil
|
||||
}
|
||||
|
||||
func (p *DLCProvider) patchOrderQueue(orderID, gameAccount, bindUUID, goodsNo, goodsName string) error {
|
||||
@@ -632,4 +596,3 @@ func (p *DLCProvider) signProxy(path, method string, data interface{}, out inter
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -179,6 +180,39 @@ func TestSignProxyDoesNotRetryOnRateLimit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDLCCreateOrderQueueKeepsOriginalOrderSN(t *testing.T) {
|
||||
const orderNo = "FO20261006180010b19fbe66ec3"
|
||||
var requestedSN string
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Path string `json:"path"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
requestedSN, _ = body.Data["order_sn"].(string)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "该订单已有发货记录,请续发未完成项或重试失败项",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
p := NewDLCProvider(server.URL, "dlc", &http.Client{Timeout: 5 * time.Second})
|
||||
_, sn, err := p.createOrderQueue("good-1", orderNo, "suit_romantic_destiny")
|
||||
if err == nil {
|
||||
t.Fatal("已有发货记录时应返回代练仓原文,不能换成新单号重试")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "请续发未完成项或重试失败项") {
|
||||
t.Fatalf("应保留代练仓原文,实际: %v", err)
|
||||
}
|
||||
if requestedSN != orderNo || sn != orderNo || strings.Contains(requestedSN, "_") {
|
||||
t.Fatalf("请求单号被改写: requested=%s returned=%s", requestedSN, sn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignProxyDoesNotRetryOnFormalOrderCreation(t *testing.T) {
|
||||
var callCount int32
|
||||
|
||||
@@ -211,5 +245,3 @@ func TestSignProxyDoesNotRetryOnFormalOrderCreation(t *testing.T) {
|
||||
t.Fatalf("正式下单接口预期只调用 1 次且不触发重试,实际调用: %d", finalCount)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user