Files
jiu/docs/review/stock-order-bugs.md
T
wangjia 66e54af8c6 feat: 商品追踪页面 + 出库提交库存校验
后端:
- feat(backend): 重命名 Batches→Products 接口,新增库存状态(在售/已卖出)和买家信息
- feat(backend): 出库单创建/提交时校验仓库库存,不足则返回明确错误信息
- fix(backend): 出库创建 status 判断逻辑修复(空值默认 draft)

前端:
- feat(client): 批次追踪改为商品追踪,新增状态列(在售/已卖出)和买家/时间列
- fix(client): 无批次号时显示"无批次"而非空
- refactor(client): BatchRecord → ProductTrackingRecord,repository 接口更新

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 22:26:42 +08:00

2.7 KiB
Raw Blame History

入库/出库单 Bug 报告

BUG-001

严重程度:中

问题描述 创建入库单(POST /api/v1/stock-in/orders)和出库单(POST /api/v1/stock-out/orders)时,缺少对 warehouse_idbinding:"required" 验证。当客户端不传 warehouse_id 或传入 0,后端会静默接受并创建一条 warehouse_id=0 的无效单据,而不是返回 400 Bad Request。

复现步骤

  1. 调用 POST /api/v1/stock-in/orders,请求体中省略 warehouse_id 字段
  2. 期望返回 400,实际返回 201,并创建了一条 warehouse_id=0 的单据

等效对出库单同样适用。

失败的测试用例

func TestStockInHandler_Create_MissingWarehouse(t *testing.T) {
    // 缺少 warehouse_id,应该返回 400
    w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
        "order_date": time.Now().Format(time.RFC3339),
        "items":      []map[string]interface{}{},
    })
    assert.Equal(t, http.StatusBadRequest, w.Code) // 实际返回 201
}

根因分析

  • backend/internal/model/stock.goStockInOrderStockOutOrder 结构体中,WarehouseID 字段缺少 binding:"required" 标签
  • 当前定义:WarehouseID uint64 \gorm:"not null" json:"warehouse_id"``
  • 缺少:binding:"required"

修复建议StockInOrderStockOutOrderWarehouseID 字段上添加 binding:"required" 标签:

WarehouseID  uint64  `gorm:"not null" json:"warehouse_id" binding:"required"`

注意:Go 的 binding:"required"uint64 类型的判断是零值(即 0)视为未填写,因此可正确拦截缺失或为 0 的情况。


BUG-002

严重程度:低

问题描述 WarehouseHandler.Delete 在资源不存在时返回 200 而不是 404。当用户删除一个不存在的仓库 ID 时,业务逻辑没有检查 RowsAffected,始终返回 200 OK。

复现步骤

  1. 调用 DELETE /api/v1/warehouses/99999(不存在的 ID
  2. 期望返回 404,实际返回 200

根因分析 backend/internal/handler/warehouse.goDelete 方法未检查 result.RowsAffected,与 ProductHandler.Delete 的实现不一致。

修复建议 参考 ProductHandler.Delete 的实现,添加 RowsAffected == 0 检查:

func (h *WarehouseHandler) Delete(c *gin.Context) {
    shopID := middleware.GetShopID(c)
    now := timeNow()
    result := h.db.Model(&model.Warehouse{}).
        Where("id = ? AND shop_id = ? AND deleted_at IS NULL", c.Param("id"), shopID).
        Update("deleted_at", now)
    if result.RowsAffected == 0 {
        c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
        return
    }
    c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}