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
+116
View File
@@ -0,0 +1,116 @@
# 参数校验 Bug 报告
## BUG-001
**严重程度**:高
**问题描述**
`POST /api/v1/partners` 接口缺少对 `name` 字段的必填校验。传入空 `name` 时,handler 成功通过 `ShouldBindJSON` 绑定并返回 201,而不是期望的 400。
**复现步骤**
1. 调用 `POST /api/v1/partners`body 为 `{"type":"supplier"}`(不含 name
2. 期望返回 400,实际返回 201
**失败的测试用例**
```go
func TestPartnerHandler_Create_MissingName(t *testing.T) {
// 这个测试会失败,揭示了 partner handler 缺少 name 必填校验
body := `{"type":"supplier"}`
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/partners", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
// 预期 400,实际得到 201
}
```
**根因分析**
`backend/internal/model/partner.go` 第 6 行:
```go
Name string `gorm:"size:200;not null" json:"name"`
```
结构体 `Name` 字段只有 GORM 的 `not null` 约束,没有 `binding:"required"` 标签。
`c.ShouldBindJSON` 只校验 `binding` 标签,不校验 GORM 标签,导致空 name 可以通过校验。
SQLite in-memory 不严格强制 NOT NULL 约束(与 MySQL 行为不同),所以数据库层面也未报错。
**修复建议**
`Partner` model 的 `Name` 字段添加 `binding:"required"` 标签:
```go
Name string `gorm:"size:200;not null" json:"name" binding:"required"`
```
或者在 handler 中手动校验:
```go
if req.Name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
return
}
```
---
## BUG-002
**严重程度**:高
**问题描述**
`POST /api/v1/stock-in/orders` 接口缺少对 `warehouse_id` 字段的必填校验。传入 `warehouse_id: 0`(JSON 中省略该字段时默认为 0)时,handler 返回 201,而不是期望的 400。
**复现步骤**
1. 调用 `POST /api/v1/stock-in/orders`body 为 `{"items":[]}`(不含 warehouse_id
2. 期望返回 400,实际返回 201
**失败的测试用例**
```go
func TestStockInHandler_Create_MissingWarehouse(t *testing.T) {
body := `{"items":[]}`
// 预期 400,实际得到 201
}
```
**根因分析**
`backend/internal/model/stock.go` 第 10 行:
```go
WarehouseID uint64 `gorm:"not null" json:"warehouse_id"`
```
同 BUG-001,结构体只有 GORM 约束,缺少 `binding:"required"`
对于 `uint64` 类型,JSON 省略时默认值为 0`ShouldBindJSON` 不会报错。
**修复建议**
`StockInOrder.WarehouseID` 添加 `binding:"required,min=1"` 标签:
```go
WarehouseID uint64 `gorm:"not null" json:"warehouse_id" binding:"required,min=1"`
```
或在 handler `Create` 函数中(`backend/internal/handler/stock_in.go`,约第 82 行)手动校验:
```go
if req.WarehouseID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "warehouse_id is required"})
return
}
```
---
## BUG-003
**严重程度**:高
**问题描述**
`POST /api/v1/stock-out/orders` 接口缺少对 `warehouse_id` 字段的必填校验,与 BUG-002 问题相同。
**复现步骤**
1. 调用 `POST /api/v1/stock-out/orders`body 为 `{"items":[]}`(不含 warehouse_id
2. 期望返回 400,实际返回 201
**失败的测试用例**
```go
func TestStockOutHandler_Create_MissingWarehouse(t *testing.T) {
body := `{"items":[]}`
// 预期 400,实际得到 201
}
```
**根因分析**
同 BUG-002`backend/internal/model/stock.go``StockOutOrder.WarehouseID` 缺少 `binding` 校验标签。
**修复建议**
`StockOutOrder.WarehouseID` 添加 `binding:"required,min=1"` 标签,或在 `backend/internal/handler/stock_out.go``Create` 函数中手动校验。