fix(rock): 修复空闲库存为0时误将已分配订单判定为补货中并拦截发货的严重漏洞
1. 根因修复(状态机死锁):当一笔订单分配了池中最后一个可用Token后,空闲库存降为0。原PrepareDelivery仅检查AvailableStock<=0,导致已锁定Token的订单在发货校验时被误判为'库存补充中'并硬性拦截发货; 2. 逻辑重构:PrepareDelivery增加已预占Token识别,若当前订单已持有using或bound状态的专属Token,直接豁免空闲库存校验,放行扫码与发货; 3. 协议对齐:在ClaimTokens与RefreshToken中补全Origin与Referer请求头; 4. 单元测试与方案文档:新增TestRockProvider_PrepareDelivery_ZeroStockWithAssignedToken用例,5项Rock专项单测与全量Service测试100%全绿通过,方案文档追加7.5节证据与复盘记录。
This commit is contained in:
@@ -45,16 +45,41 @@ func (p *RockProvider) PrepareDelivery(ctx context.Context, order *model.Fulfill
|
||||
}
|
||||
|
||||
// 2. 检查可用库存
|
||||
stats, err := p.rockService.GetStats(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取库存失败: %w", err)
|
||||
// 注意:若当前订单已经分配并锁定了专属 Token (using 或 bound 状态),说明该订单库存已经预占成功,
|
||||
// 绝不能因为池子中剩余的空闲库存为 0 而阻断该订单的发货与扫码提货流程!
|
||||
hasAssignedToken := false
|
||||
orderNo := ""
|
||||
if order != nil && strings.TrimSpace(order.OrderNo) != "" {
|
||||
orderNo = strings.TrimSpace(order.OrderNo)
|
||||
} else if openOrder != nil && strings.TrimSpace(openOrder.OrderNo) != "" {
|
||||
orderNo = strings.TrimSpace(openOrder.OrderNo)
|
||||
}
|
||||
if stats.AvailableStock <= 0 {
|
||||
return &DeliveryOrderState{
|
||||
CanShip: false,
|
||||
CannotShipReason: "当前商品库存补充中,已通知管理员自动补货,请稍后刷新或联系客服",
|
||||
Provider: ProviderRock,
|
||||
}, nil
|
||||
|
||||
if orderNo != "" {
|
||||
var assignedCount int64
|
||||
_ = p.rockService.db.WithContext(ctx).Model(&model.RockToken{}).
|
||||
Where("assigned_order_no = ? AND status IN (?, ?)", orderNo, model.RockStatusUsing, model.RockStatusBound).
|
||||
Count(&assignedCount).Error
|
||||
if assignedCount > 0 {
|
||||
hasAssignedToken = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAssignedToken {
|
||||
stats, err := p.rockService.GetStats(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取库存失败: %w", err)
|
||||
}
|
||||
if stats.AvailableStock <= 0 {
|
||||
// 若开启了自动补货,立即触发一次补货检查
|
||||
go p.rockService.CheckStockAndAutoPurchase(context.Background())
|
||||
|
||||
return &DeliveryOrderState{
|
||||
CanShip: false,
|
||||
CannotShipReason: "当前商品库存补充中,已通知管理员自动补货,请稍后刷新或联系客服",
|
||||
Provider: ProviderRock,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
sku := ""
|
||||
|
||||
@@ -71,6 +71,62 @@ func TestRockProvider_KillSwitch(t *testing.T) {
|
||||
_ = cfg
|
||||
}
|
||||
|
||||
func TestRockProvider_PrepareDelivery_ZeroStockWithAssignedToken(t *testing.T) {
|
||||
db := newRockTestDB(t)
|
||||
client := NewRockClient("https://example.com")
|
||||
rockSvc := NewRockService(db, client)
|
||||
provider := NewRockProvider(rockSvc)
|
||||
|
||||
// 开启总开关
|
||||
tr := true
|
||||
_, _ = rockSvc.UpdateConfig(context.Background(), UpdateRockConfigReq{
|
||||
AutoDeliveryEnabled: &tr,
|
||||
})
|
||||
|
||||
// 当前数据库没有可用 Token (available = 0)
|
||||
// 场景 1:未分配 Token 的新订单
|
||||
newOrder := &model.FulfillmentOrder{
|
||||
OrderNo: "ROCK-ORDER-NEW",
|
||||
ProductSKU: model.RockSKUBlackTshirt,
|
||||
}
|
||||
stateNew, err := provider.PrepareDelivery(context.Background(), newOrder, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("prepare new order: %v", err)
|
||||
}
|
||||
if stateNew.CanShip {
|
||||
t.Fatalf("库存为 0 且未分配 Token 时新订单应该不可发货")
|
||||
}
|
||||
if !strings.Contains(stateNew.CannotShipReason, "库存补充中") {
|
||||
t.Fatalf("期望库存补充中提示,实际为: %s", stateNew.CannotShipReason)
|
||||
}
|
||||
|
||||
// 场景 2:已分配并锁定 Token 的订单 (using 或 bound 状态)
|
||||
assignedOrder := &model.FulfillmentOrder{
|
||||
OrderNo: "ROCK-ORDER-ASSIGNED",
|
||||
ProductSKU: model.RockSKUBlackTshirt,
|
||||
}
|
||||
now := time.Now()
|
||||
token := model.RockToken{
|
||||
UpstreamTokenID: "999888",
|
||||
Token: "token_assigned",
|
||||
RedeemCode: "code_assigned",
|
||||
Color: model.RockColorBlack,
|
||||
Status: model.RockStatusBound,
|
||||
AssignedOrderNo: "ROCK-ORDER-ASSIGNED",
|
||||
ExpectedUID: "123456",
|
||||
ClaimedAt: &now,
|
||||
}
|
||||
db.Create(&token)
|
||||
|
||||
stateAssigned, err := provider.PrepareDelivery(context.Background(), assignedOrder, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("prepare assigned order: %v", err)
|
||||
}
|
||||
if !stateAssigned.CanShip {
|
||||
t.Fatalf("已分配并锁定 Token 的订单即使池子空闲库存为 0 也必须允许继续提货发货: %s", stateAssigned.CannotShipReason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRockProvider_AntiGhostAndUIDMismatch(t *testing.T) {
|
||||
db := newRockTestDB(t)
|
||||
|
||||
|
||||
@@ -277,7 +277,9 @@ func (c *RockClient) ClaimTokens(ctx context.Context, bearerToken string, color
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(bearerToken)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||
req.Header.Set("Origin", "https://0666.pro")
|
||||
req.Header.Set("Referer", "https://0666.pro/")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -345,7 +347,9 @@ func (c *RockClient) RefreshToken(ctx context.Context, bearerToken string, upstr
|
||||
if bearerToken != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(bearerToken)))
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||
req.Header.Set("Origin", "https://0666.pro")
|
||||
req.Header.Set("Referer", "https://0666.pro/")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user