From 13a6777a85682d1f0515be4b91cad87e56b7087d Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sun, 21 Jun 2026 19:46:30 +0800 Subject: [PATCH] chore: release server-v1.0.73 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 | 23 ++++++++++++++++----- backend/internal/handler/stock_in_test.go | 19 +++++++++++++++-- backend/internal/handler/stock_out.go | 23 ++++++++++++++++----- backend/internal/handler/testhelper_test.go | 4 ++-- backend/internal/router/router.go | 8 +++---- 6 files changed, 64 insertions(+), 18 deletions(-) diff --git a/CHANGELOG-server.md b/CHANGELOG-server.md index 6bef215..2fb6c18 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.73] - 2026-06-21 + +### 改进 +- 撤回审核中单据的权限放宽:管理员/超管可撤回任意单据,普通操作员可撤回本人提交的单据(此前仅管理员) + ## [1.0.72] - 2026-06-21 ### 新功能 diff --git a/backend/internal/handler/stock_in.go b/backend/internal/handler/stock_in.go index 583d302..7dfbdfb 100644 --- a/backend/internal/handler/stock_in.go +++ b/backend/internal/handler/stock_in.go @@ -257,16 +257,29 @@ func (h *StockInHandler) Reject(c *gin.Context) { } // Withdraw PUT /api/v1/stock-in/orders/:id/withdraw -// 审核中(pending)撤回为草稿(draft),供管理员修改后重新提交。仅 pending 可撤回; +// 审核中(pending)撤回为草稿(draft),修改后可重新提交。仅 pending 可撤回; // 已审核(approved)单据只读、库存已变动,不可撤回。 +// 权限:管理员/超管可撤回任意单;普通操作员只能撤回本人提交的单(operator_id 为本人)。 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 { + userID := middleware.GetUserID(c) + role := middleware.GetRole(c) + + var order model.StockInOrder + if err := h.db.Where("id = ? AND shop_id = ? AND status = 'pending'", c.Param("id"), shopID). + First(&order).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in pending status"}) return } + + isAdmin := role == "admin" || role == "superadmin" + if !isAdmin && order.OperatorID != userID { + c.JSON(http.StatusForbidden, gin.H{"error": "只能撤回本人提交的单据"}) + return + } + + h.db.Model(&model.StockInOrder{}). + Where("id = ? AND shop_id = ?", order.ID, shopID). + Update("status", "draft") 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 3f6578c..77cc4fb 100644 --- a/backend/internal/handler/stock_in_test.go +++ b/backend/internal/handler/stock_in_test.go @@ -204,11 +204,11 @@ func TestStockInHandler_Withdraw(t *testing.T) { orderID := extractID(w) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), adminToken, nil) - // 1. 非管理员撤回 → 403 + // 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 + // 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) @@ -221,6 +221,21 @@ func TestStockInHandler_Withdraw(t *testing.T) { // 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) + + // 5. 操作员撤回「本人」提交的单 → 200(自己发的肯定能撤) + w = makeRequest(r, "POST", "/api/v1/stock-in/orders", opToken, map[string]interface{}{ + "warehouse_id": warehouse.ID, + "order_date": time.Now().Format(time.RFC3339), + "items": []map[string]interface{}{ + {"product_id": product.ID, "quantity": 3.0}, + }, + }) + ownID := extractID(w) + makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", ownID), opToken, nil) + w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/withdraw", ownID), opToken, nil) + assert.Equal(t, http.StatusOK, w.Code) + w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-in/orders/%d", ownID), opToken, nil) + assert.Equal(t, "draft", parseResponse(w)["data"].(map[string]interface{})["status"]) } func TestStockInHandler_GetNotFound(t *testing.T) { diff --git a/backend/internal/handler/stock_out.go b/backend/internal/handler/stock_out.go index 1c0956c..096906d 100644 --- a/backend/internal/handler/stock_out.go +++ b/backend/internal/handler/stock_out.go @@ -242,16 +242,29 @@ func (h *StockOutHandler) Reject(c *gin.Context) { } // Withdraw PUT /api/v1/stock-out/orders/:id/withdraw -// 审核中(pending)撤回为草稿(draft),供管理员修改后重新提交。仅 pending 可撤回; +// 审核中(pending)撤回为草稿(draft),修改后可重新提交。仅 pending 可撤回; // 已审核(approved)单据只读、库存已变动,不可撤回。 +// 权限:管理员/超管可撤回任意单;普通操作员只能撤回本人提交的单(operator_id 为本人)。 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 { + userID := middleware.GetUserID(c) + role := middleware.GetRole(c) + + var order model.StockOutOrder + if err := h.db.Where("id = ? AND shop_id = ? AND status = 'pending'", c.Param("id"), shopID). + First(&order).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in pending status"}) return } + + isAdmin := role == "admin" || role == "superadmin" + if !isAdmin && order.OperatorID != userID { + c.JSON(http.StatusForbidden, gin.H{"error": "只能撤回本人提交的单据"}) + return + } + + h.db.Model(&model.StockOutOrder{}). + Where("id = ? AND shop_id = ?", order.ID, shopID). + Update("status", "draft") 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 acce42f..195b0e7 100644 --- a/backend/internal/handler/testhelper_test.go +++ b/backend/internal/handler/testhelper_test.go @@ -63,7 +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) + stockIn.PUT("/orders/:id/withdraw", stockInH.Withdraw) // 出库路由 stockOut := api.Group("/stock-out") @@ -73,7 +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) + stockOut.PUT("/orders/:id/withdraw", stockOutH.Withdraw) // 库存路由 inv := api.Group("/inventory") diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 80be46e..fdb577c 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -153,8 +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) + // 撤回(审核中→草稿):管理员/超管任意单,操作员限本人单(handler 内判权) + stockIn.PUT("/orders/:id/withdraw", stockInH.Withdraw) } // 出库 @@ -168,8 +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) + // 撤回(审核中→草稿):管理员/超管任意单,操作员限本人单(handler 内判权) + stockOut.PUT("/orders/:id/withdraw", stockOutH.Withdraw) } // 库存