feat(backend): 库存按商品过滤 + 香型(分类)字典 CRUD
商品域富数据/重组所需后端: - GET /inventory 加 product_id 过滤 → 解锁商品详情「当前库存/货值/各仓库分布」 (配合已支持 product_id 的 /inventory/logs,详情可拼出大半富数据) - 新增 /product-options/categories 香型字典 CRUD(ProductCategory 模型已存在, 补管理接口)→ 商品列表 5-tab 重组的最后一块字典 均守多租户 shop_id;新增 3 个测试(FilterByProduct/CategoryCRUD/CategoryIsolation)。 go build/vet/test 全过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
@@ -84,3 +85,73 @@ func TestProductOptionHandler_ListNamesIsolation(t *testing.T) {
|
||||
data := parseResponse(w)["data"].([]interface{})
|
||||
assert.Len(t, data, 0)
|
||||
}
|
||||
|
||||
// setupCategoryRouter 注册 香型/分类 字典 CRUD 路由(含 JWT)。
|
||||
func setupCategoryRouter(db *gorm.DB) *gin.Engine {
|
||||
h := NewProductOptionHandler(db)
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
api := r.Group("/api/v1")
|
||||
api.Use(middleware.JWT(db))
|
||||
cats := api.Group("/product-options/categories")
|
||||
cats.GET("", h.ListCategories)
|
||||
cats.POST("", h.CreateCategory)
|
||||
cats.PUT("/:id", h.UpdateCategory)
|
||||
cats.DELETE("/:id", h.DeleteCategory)
|
||||
return r
|
||||
}
|
||||
|
||||
func TestProductOptionHandler_CategoryCRUD(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "CAT001")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupCategoryRouter(db)
|
||||
|
||||
// 1. 创建
|
||||
w := makeRequest(r, "POST", "/api/v1/product-options/categories", token,
|
||||
map[string]interface{}{"name": "酱香"})
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
id := parseResponse(w)["data"].(map[string]interface{})["id"]
|
||||
|
||||
// 2. 列表命中
|
||||
w = makeRequest(r, "GET", "/api/v1/product-options/categories", token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
data := parseResponse(w)["data"].([]interface{})
|
||||
assert.Len(t, data, 1)
|
||||
assert.Equal(t, "酱香", data[0].(map[string]interface{})["name"])
|
||||
|
||||
// 3. 更新
|
||||
w = makeRequest(r, "PUT",
|
||||
fmt.Sprintf("/api/v1/product-options/categories/%v", id), token,
|
||||
map[string]interface{}{"name": "浓香"})
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
w = makeRequest(r, "GET", "/api/v1/product-options/categories?keyword=浓", token, nil)
|
||||
data = parseResponse(w)["data"].([]interface{})
|
||||
assert.Len(t, data, 1)
|
||||
|
||||
// 4. 删除
|
||||
w = makeRequest(r, "DELETE",
|
||||
fmt.Sprintf("/api/v1/product-options/categories/%v", id), token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
w = makeRequest(r, "GET", "/api/v1/product-options/categories", token, nil)
|
||||
assert.Len(t, parseResponse(w)["data"].([]interface{}), 0)
|
||||
}
|
||||
|
||||
func TestProductOptionHandler_CategoryIsolation(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shopA := testutil.CreateTestShop(db, "CAT_A")
|
||||
userA := testutil.CreateTestUser(db, shopA.ID, "adminA", "pass", "admin")
|
||||
tokenA := getAuthToken(userA.ID, shopA.ID, "admin")
|
||||
shopB := testutil.CreateTestShop(db, "CAT_B")
|
||||
r := setupCategoryRouter(db)
|
||||
|
||||
db.Create(&model.ProductCategory{
|
||||
TenantBase: model.TenantBase{ShopID: shopB.ID}, Name: "他店香型",
|
||||
})
|
||||
|
||||
// A 店查不到 B 店香型(shop_id 隔离)
|
||||
w := makeRequest(r, "GET", "/api/v1/product-options/categories?keyword=他店", tokenA, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Len(t, parseResponse(w)["data"].([]interface{}), 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user