Files
affiliate_dash/backend/internal/service/provider_dlc_test.go
T
yml2213 af0822949d fix(delivery): 修复 DLC 正式下单重复提交与发货并发冲突问题
1. DLC 正式下单接口禁用传输层自动重试:/public/users/orders 属于非幂等状态变更接口,发生频控或抖动时由上层回调与状态同步接管,严禁盲目原单号重试导致上游抛出“该子订单已处理,请勿重复提交”;
2. 扩充队列防重与已有记录识别:识别上游“该订单已有发货记录/已处理”错误,重新建队时自动追加独立重试单号(_xxxx)并透传至正式下单,避免单号污染冲突;
3. 上游回调支持重试单号兼容:HandleShipNotify 在精准匹配未命中时,自动按下划线前缀匹配主订单号(FO..._xxxx -> FO...),保障重试单回调正常交付;
4. 加强提交并发锁防护:在 35 秒活跃执行窗口内对 in_progress 阶段实行严格并发互斥,避免上游 Webhook 提前推回失败状态导致后续重试穿透产生并发打架;提单失败时显式重置 provider_order_stage 释放锁。
2026-09-18 16:19:37 +08:00

176 lines
4.9 KiB
Go

package service
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"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, "", true},
{nil, 502, "", true},
{nil, 503, "", true},
{nil, 504, "", true},
{nil, 400, "绑定校验失败: 请求过于频繁,请稍后再试", true},
{nil, 400, "请求过于频繁", true},
{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 TestSignProxyRetriesOnRateLimitAndSucceeds(t *testing.T) {
var callCount int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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)
}
}
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)
}
}