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>
This commit is contained in:
wangjia
2026-04-10 22:26:42 +08:00
parent ce1cbf404c
commit 66e54af8c6
12 changed files with 513 additions and 68 deletions
+72
View File
@@ -0,0 +1,72 @@
# 入库/出库单 Bug 报告
## BUG-001
**严重程度**:中
**问题描述**
创建入库单(`POST /api/v1/stock-in/orders`)和出库单(`POST /api/v1/stock-out/orders`)时,缺少对 `warehouse_id``binding:"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` 的单据
等效对出库单同样适用。
**失败的测试用例**
```go
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.go``StockInOrder``StockOutOrder` 结构体中,`WarehouseID` 字段缺少 `binding:"required"` 标签
- 当前定义:`WarehouseID uint64 \`gorm:"not null" json:"warehouse_id"\``
- 缺少:`binding:"required"`
**修复建议**
`StockInOrder``StockOutOrder``WarehouseID` 字段上添加 `binding:"required"` 标签:
```go
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.go``Delete` 方法未检查 `result.RowsAffected`,与 `ProductHandler.Delete` 的实现不一致。
**修复建议**
参考 `ProductHandler.Delete` 的实现,添加 `RowsAffected == 0` 检查:
```go
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"})
}
```