From 7a78448a253f3fb3459ea11e0b92a2f931aebb87 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sun, 21 Jun 2026 19:09:30 +0800 Subject: [PATCH] chore: release server-v1.0.72 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx --- CHANGELOG-server.md | 5 +++ backend/internal/handler/stock_in.go | 15 ++++++++ backend/internal/handler/stock_in_test.go | 41 +++++++++++++++++++++ backend/internal/handler/stock_out.go | 15 ++++++++ backend/internal/handler/testhelper_test.go | 2 + backend/internal/router/router.go | 4 ++ 6 files changed, 82 insertions(+) diff --git a/CHANGELOG-server.md b/CHANGELOG-server.md index c73e824..6bef215 100644 --- a/CHANGELOG-server.md +++ b/CHANGELOG-server.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.72] - 2026-06-21 + +### 新功能 +- 审核中(待审核)的入库单/出库单,管理员可「撤回」为草稿,修改后重新提交审核;已审核单据仍只读不可撤回 + ## [1.0.71] - 2026-06-21 ### 改进 diff --git a/backend/internal/handler/stock_in.go b/backend/internal/handler/stock_in.go index 9a725ba..583d302 100644 --- a/backend/internal/handler/stock_in.go +++ b/backend/internal/handler/stock_in.go @@ -255,3 +255,18 @@ func (h *StockInHandler) Reject(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"message": "rejected"}) } + +// Withdraw PUT /api/v1/stock-in/orders/:id/withdraw +// 审核中(pending)撤回为草稿(draft),供管理员修改后重新提交。仅 pending 可撤回; +// 已审核(approved)单据只读、库存已变动,不可撤回。 +func (h *StockInHandler) Withdraw(c *gin.Context) { + shopID := middleware.GetShopID(c) + result := h.db.Model(&model.StockInOrder{}). + Where("id = ? AND shop_id = ? AND status = 'pending'", c.Param("id"), shopID). + Update("status", "draft") + if result.RowsAffected == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in pending status"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "withdrawn"}) +} diff --git a/backend/internal/handler/stock_in_test.go b/backend/internal/handler/stock_in_test.go index aaf4e9f..3f6578c 100644 --- a/backend/internal/handler/stock_in_test.go +++ b/backend/internal/handler/stock_in_test.go @@ -182,6 +182,47 @@ func TestStockInHandler_Reject(t *testing.T) { assert.Equal(t, "rejected", detailData["status"]) } +func TestStockInHandler_Withdraw(t *testing.T) { + db := testutil.SetupTestDB() + shop := testutil.CreateTestShop(db, "SI006") + admin := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") + operator := testutil.CreateTestUser(db, shop.ID, "op", "pass", "operator") + warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse") + product := testutil.CreateTestProduct(db, shop.ID, "Tequila") + adminToken := getAuthToken(admin.ID, shop.ID, "admin") + opToken := getAuthToken(operator.ID, shop.ID, "operator") + r := setupProtectedRouter(db) + + // 创建并提交(进入 pending) + w := makeRequest(r, "POST", "/api/v1/stock-in/orders", adminToken, map[string]interface{}{ + "warehouse_id": warehouse.ID, + "order_date": time.Now().Format(time.RFC3339), + "items": []map[string]interface{}{ + {"product_id": product.ID, "quantity": 5.0}, + }, + }) + orderID := extractID(w) + makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), adminToken, nil) + + // 1. 非管理员撤回 → 403 + w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/withdraw", orderID), opToken, nil) + assert.Equal(t, http.StatusForbidden, w.Code) + + // 2. 管理员撤回 → 200,状态回到 draft + w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/withdraw", orderID), adminToken, nil) + assert.Equal(t, http.StatusOK, w.Code) + w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-in/orders/%d", orderID), adminToken, nil) + assert.Equal(t, "draft", parseResponse(w)["data"].(map[string]interface{})["status"]) + + // 3. 撤回后已是 draft,再撤回 → 400(仅 pending 可撤回) + w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/withdraw", orderID), adminToken, nil) + assert.Equal(t, http.StatusBadRequest, w.Code) + + // 4. 撤回为 draft 后可再次修改并提交 + w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), adminToken, nil) + assert.Equal(t, http.StatusOK, w.Code) +} + func TestStockInHandler_GetNotFound(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "SI006") diff --git a/backend/internal/handler/stock_out.go b/backend/internal/handler/stock_out.go index 9c0eac9..1c0956c 100644 --- a/backend/internal/handler/stock_out.go +++ b/backend/internal/handler/stock_out.go @@ -240,3 +240,18 @@ func (h *StockOutHandler) Reject(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"message": "rejected"}) } + +// Withdraw PUT /api/v1/stock-out/orders/:id/withdraw +// 审核中(pending)撤回为草稿(draft),供管理员修改后重新提交。仅 pending 可撤回; +// 已审核(approved)单据只读、库存已变动,不可撤回。 +func (h *StockOutHandler) Withdraw(c *gin.Context) { + shopID := middleware.GetShopID(c) + result := h.db.Model(&model.StockOutOrder{}). + Where("id = ? AND shop_id = ? AND status = 'pending'", c.Param("id"), shopID). + Update("status", "draft") + if result.RowsAffected == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in pending status"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "withdrawn"}) +} diff --git a/backend/internal/handler/testhelper_test.go b/backend/internal/handler/testhelper_test.go index 9b5c039..acce42f 100644 --- a/backend/internal/handler/testhelper_test.go +++ b/backend/internal/handler/testhelper_test.go @@ -63,6 +63,7 @@ func setupProtectedRouter(db *gorm.DB) *gin.Engine { stockIn.PUT("/orders/:id/submit", stockInH.Submit) stockIn.PUT("/orders/:id/approve", stockInH.Approve) stockIn.PUT("/orders/:id/reject", stockInH.Reject) + stockIn.PUT("/orders/:id/withdraw", middleware.AdminOnly(), stockInH.Withdraw) // 出库路由 stockOut := api.Group("/stock-out") @@ -72,6 +73,7 @@ func setupProtectedRouter(db *gorm.DB) *gin.Engine { stockOut.PUT("/orders/:id/submit", stockOutH.Submit) stockOut.PUT("/orders/:id/approve", stockOutH.Approve) stockOut.PUT("/orders/:id/reject", stockOutH.Reject) + stockOut.PUT("/orders/:id/withdraw", middleware.AdminOnly(), stockOutH.Withdraw) // 库存路由 inv := api.Group("/inventory") diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 2457c5b..80be46e 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -153,6 +153,8 @@ func Setup(r *gin.Engine, db *gorm.DB) { stockIn.PUT("/orders/:id/submit", stockInH.Submit) stockIn.PUT("/orders/:id/approve", stockInH.Approve) stockIn.PUT("/orders/:id/reject", stockInH.Reject) + // 撤回(审核中→草稿)仅管理员 + stockIn.PUT("/orders/:id/withdraw", middleware.AdminOnly(), stockInH.Withdraw) } // 出库 @@ -166,6 +168,8 @@ func Setup(r *gin.Engine, db *gorm.DB) { stockOut.PUT("/orders/:id/submit", stockOutH.Submit) stockOut.PUT("/orders/:id/approve", stockOutH.Approve) stockOut.PUT("/orders/:id/reject", stockOutH.Reject) + // 撤回(审核中→草稿)仅管理员 + stockOut.PUT("/orders/:id/withdraw", middleware.AdminOnly(), stockOutH.Withdraw) } // 库存