代练仓按原单号回查能不能发货。随机后缀查不到,正式单会失败。已有队列时应续发原单,不能换号。 Co-authored-by: Cursor <cursoragent@cursor.com>
248 lines
7.3 KiB
Go
248 lines
7.3 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestIsQueueDeadError(t *testing.T) {
|
|
tests := []struct {
|
|
err error
|
|
expected bool
|
|
}{
|
|
{nil, false},
|
|
{errors.New("订单不可操作"), true},
|
|
{errors.New("补充发货账号失败: 订单不可操作"), true},
|
|
{errors.New("订单不存在"), true},
|
|
{errors.New("队列已失效"), true},
|
|
{errors.New("该订单正在处理中,请勿重复提交"), true},
|
|
{errors.New("创建发货队列失败: 该订单正在处理中,请勿重复提交"), true},
|
|
{errors.New("创建发货队列失败: 该订单已有发货记录,请续发未完成项或重试失败项"), true},
|
|
{errors.New("创建上游正式订单失败: 该子订单已处理,请勿重复提交"), true},
|
|
{errors.New("账号尚未绑定"), false},
|
|
{errors.New("请求过于频繁"), false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
got := isQueueDeadError(tc.err)
|
|
if got != tc.expected {
|
|
t.Errorf("isQueueDeadError(%v) = %v, expected %v", tc.err, got, tc.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsRateLimitError(t *testing.T) {
|
|
tests := []struct {
|
|
err error
|
|
expected bool
|
|
}{
|
|
{nil, false},
|
|
{errors.New("请求过于频繁,请稍后再试"), true},
|
|
{errors.New("系统繁忙,请稍后再试"), true},
|
|
{errors.New("Rate limit exceeded"), true},
|
|
{errors.New("Too Many Requests"), true},
|
|
{errors.New("账号尚未绑定"), false},
|
|
{errors.New("订单不存在"), false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
got := isRateLimitError(tc.err)
|
|
if got != tc.expected {
|
|
t.Errorf("isRateLimitError(%v) = %v, expected %v", tc.err, got, tc.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsTransientDeliveryError(t *testing.T) {
|
|
tests := []struct {
|
|
err error
|
|
statusCode int
|
|
message string
|
|
expected bool
|
|
}{
|
|
{errors.New("dial tcp timeout"), 0, "", true},
|
|
{nil, 429, "", false},
|
|
{nil, 502, "", true},
|
|
{nil, 503, "", true},
|
|
{nil, 504, "", true},
|
|
{nil, 400, "绑定校验失败: 请求过于频繁,请稍后再试", false},
|
|
{nil, 400, "请求过于频繁", false},
|
|
{nil, 400, "系统繁忙,请稍后再试", true},
|
|
{nil, 400, "网络繁忙", true},
|
|
{nil, 400, "账号尚未绑定,请扫码完成绑定后再提交", false},
|
|
{nil, 400, "订单不可操作", false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
got := isTransientDeliveryError(tc.err, tc.statusCode, tc.message)
|
|
if got != tc.expected {
|
|
t.Errorf("isTransientDeliveryError(%v, %d, %q) = %v, expected %v", tc.err, tc.statusCode, tc.message, got, tc.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSignProxyRetriesOnTransientErrorAndSucceeds(t *testing.T) {
|
|
var callCount int32
|
|
var capturedForwardedFor string
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
capturedForwardedFor = r.Header.Get("X-Forwarded-For")
|
|
count := atomic.AddInt32(&callCount, 1)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if count == 1 {
|
|
// 第一次调用返回系统繁忙瞬时错误
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"code": 1,
|
|
"message": "系统繁忙,请稍后再试",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 第二次重试成功
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"code": 0,
|
|
"message": "success",
|
|
"data": map[string]interface{}{
|
|
"good": map[string]interface{}{
|
|
"good_id": "test-good",
|
|
},
|
|
},
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
p := NewDLCProvider(server.URL, "dlc", client)
|
|
|
|
var out struct {
|
|
Good map[string]interface{} `json:"good"`
|
|
}
|
|
err := p.signProxy("/public/goods/detail", "POST", map[string]interface{}{
|
|
"good_id": "test-good",
|
|
}, &out)
|
|
|
|
if err != nil {
|
|
t.Fatalf("signProxy 预期在退避重试后成功,但返回错误: %v", err)
|
|
}
|
|
|
|
finalCount := atomic.LoadInt32(&callCount)
|
|
if finalCount != 2 {
|
|
t.Fatalf("预期总共调用 2 次(1次失败 + 1次重试成功),实际调用: %d", finalCount)
|
|
}
|
|
|
|
if out.Good == nil || out.Good["good_id"] != "test-good" {
|
|
t.Fatalf("预期返回商品信息,实际: %v", out.Good)
|
|
}
|
|
|
|
if capturedForwardedFor == "" {
|
|
t.Fatalf("预期包含 X-Forwarded-For 报头,实际为空")
|
|
}
|
|
}
|
|
|
|
func TestSignProxyDoesNotRetryOnRateLimit(t *testing.T) {
|
|
var callCount int32
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&callCount, 1)
|
|
w.WriteHeader(http.StatusTooManyRequests)
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"code": 1,
|
|
"message": "请求过于频繁,请稍后再试",
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
p := NewDLCProvider(server.URL, "dlc", client)
|
|
|
|
var out struct {
|
|
Good map[string]interface{} `json:"good"`
|
|
}
|
|
err := p.signProxy("/public/goods/detail", "POST", map[string]interface{}{
|
|
"good_id": "test-good",
|
|
}, &out)
|
|
|
|
if err == nil {
|
|
t.Fatalf("预期返回错误,但返回成功")
|
|
}
|
|
|
|
finalCount := atomic.LoadInt32(&callCount)
|
|
if finalCount != 1 {
|
|
t.Fatalf("频控限流接口预期只调用 1 次且不触发盲目重试,实际调用: %d", finalCount)
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&callCount, 1)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"code": 1,
|
|
"message": "系统繁忙,请稍后再试",
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
p := NewDLCProvider(server.URL, "dlc", client)
|
|
|
|
var out struct {
|
|
Order map[string]interface{} `json:"order"`
|
|
}
|
|
err := p.signProxy("/public/users/orders", "POST", map[string]interface{}{
|
|
"order_sn": "FO123456",
|
|
}, &out)
|
|
|
|
if err == nil {
|
|
t.Fatalf("预期返回错误,但返回成功")
|
|
}
|
|
|
|
finalCount := atomic.LoadInt32(&callCount)
|
|
if finalCount != 1 {
|
|
t.Fatalf("正式下单接口预期只调用 1 次且不触发重试,实际调用: %d", finalCount)
|
|
}
|
|
}
|