From e012f6e02b63b9885b21a4ecfb93ff01e1edaf9e Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 7 Jul 2026 16:45:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E5=BA=93=E5=AD=98=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=88=97=E5=A4=B4=E6=8E=92=E5=BA=8F=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=BA=93=E5=AD=98/=E6=88=90=E6=9C=AC=E4=BB=B7/=E6=80=BB?= =?UTF-8?q?=E4=BB=B7/=E7=94=9F=E4=BA=A7=E6=97=A5=E6=9C=9F/=E5=85=A5?= =?UTF-8?q?=E5=BA=93=E6=97=B6=E9=97=B4=E5=85=A8=E5=B1=80=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sort_by 白名单列映射 SQL 表达式(cost/price/prod_date 与 SELECT 同 COALESCE 口径,排序与显示一致)+ sort_dir,非法值忽略回默认;id 兜底保证分页稳定。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o --- backend/internal/handler/inventory.go | 19 +++++++++- backend/internal/handler/inventory_test.go | 42 ++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/backend/internal/handler/inventory.go b/backend/internal/handler/inventory.go index face413..0acfce3 100644 --- a/backend/internal/handler/inventory.go +++ b/backend/internal/handler/inventory.go @@ -158,6 +158,23 @@ func (h *InventoryHandler) List(c *gin.Context) { return } + // 排序(2026-07-07 列头排序):白名单列 → SQL 表达式,防注入;带 id 兜底保证分页稳定。 + // cost/price/prod_date 用与 SELECT 相同的 COALESCE 口径,排序结果与显示一致。 + orderExpr := "inv.id DESC" + if sortCol, ok := map[string]string{ + "qty": "inv.quantity", + "cost": "COALESCE(sii.cost_price, inv.unit_price, p.purchase_price)", + "price": "inv.quantity * COALESCE(sii.cost_price, inv.unit_price, p.purchase_price)", + "prod_date": "COALESCE(DATE(sii.production_date), DATE(inv.production_date))", + "in_time": "inv.created_at", + }[c.Query("sort_by")]; ok { + dir := "ASC" + if c.Query("sort_dir") == "desc" { + dir = "DESC" + } + orderExpr = sortCol + " " + dir + ", inv.id DESC" + } + // Data query dataSQL := ` SELECT @@ -183,7 +200,7 @@ func (h *InventoryHandler) List(c *gin.Context) { LEFT JOIN products p ON p.id = inv.product_id AND p.deleted_at IS NULL LEFT JOIN warehouses w ON w.id = inv.warehouse_id WHERE ` + baseWhere + ` - ORDER BY inv.id DESC + ORDER BY ` + orderExpr + ` LIMIT ? OFFSET ?` dataArgs := append(args, pageSize, (page-1)*pageSize) diff --git a/backend/internal/handler/inventory_test.go b/backend/internal/handler/inventory_test.go index 1869ec7..2ad9d39 100644 --- a/backend/internal/handler/inventory_test.go +++ b/backend/internal/handler/inventory_test.go @@ -3,6 +3,7 @@ package handler import ( "fmt" "net/http" + "net/http/httptest" "testing" "time" @@ -525,3 +526,44 @@ func TestInventoryHandler_StatusTenantIsolation(t *testing.T) { require.NoError(t, db.Where("id = ?", invB.ID).First(&got).Error) assert.Equal(t, "stock", got.Status) } + +// 列头排序(2026-07-07):白名单列全局排序,非法列忽略回默认 +func TestInventoryHandler_ListSort(t *testing.T) { + db := testutil.SetupTestDB() + shop := testutil.CreateTestShop(db, "INV_SORT") + user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") + wh := testutil.CreateTestWarehouse(db, shop.ID, "仓") + token := getAuthToken(user.ID, shop.ID, "admin") + r := setupProtectedRouter(db) + + for i, qty := range []float64{5, 20, 10} { + p := testutil.CreateTestProduct(db, shop.ID, fmt.Sprintf("酒%d", i)) + price := float64(100 * (i + 1)) + require.NoError(t, db.Create(&model.Inventory{ + ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &p.ID, + Quantity: qty, UnitPrice: &price, + }).Error) + } + + qtys := func(w *httptest.ResponseRecorder) []float64 { + rows := parseResponse(w)["data"].([]interface{}) + out := make([]float64, len(rows)) + for i, r := range rows { + out[i] = r.(map[string]interface{})["quantity"].(float64) + } + return out + } + + // qty 升序 + w := makeRequest(r, "GET", "/api/v1/inventory?sort_by=qty&sort_dir=asc", token, nil) + assert.Equal(t, []float64{5, 10, 20}, qtys(w)) + // qty 降序 + w = makeRequest(r, "GET", "/api/v1/inventory?sort_by=qty&sort_dir=desc", token, nil) + assert.Equal(t, []float64{20, 10, 5}, qtys(w)) + // 总价 = qty×进价:5×100=500, 20×200=4000, 10×300=3000 → 升序 5,10,20 + w = makeRequest(r, "GET", "/api/v1/inventory?sort_by=price&sort_dir=asc", token, nil) + assert.Equal(t, []float64{5, 10, 20}, qtys(w)) + // 非法列忽略 → 默认 id 倒序(最后插入的 qty=10 在前) + w = makeRequest(r, "GET", "/api/v1/inventory?sort_by=evil;drop", token, nil) + assert.Equal(t, []float64{10, 20, 5}, qtys(w)) +}