Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13a6777a85 |
@@ -5,6 +5,11 @@
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
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).
|
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
|
## [1.0.72] - 2026-06-21
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -257,16 +257,29 @@ func (h *StockInHandler) Reject(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Withdraw PUT /api/v1/stock-in/orders/:id/withdraw
|
// Withdraw PUT /api/v1/stock-in/orders/:id/withdraw
|
||||||
// 审核中(pending)撤回为草稿(draft),供管理员修改后重新提交。仅 pending 可撤回;
|
// 审核中(pending)撤回为草稿(draft),修改后可重新提交。仅 pending 可撤回;
|
||||||
// 已审核(approved)单据只读、库存已变动,不可撤回。
|
// 已审核(approved)单据只读、库存已变动,不可撤回。
|
||||||
|
// 权限:管理员/超管可撤回任意单;普通操作员只能撤回本人提交的单(operator_id 为本人)。
|
||||||
func (h *StockInHandler) Withdraw(c *gin.Context) {
|
func (h *StockInHandler) Withdraw(c *gin.Context) {
|
||||||
shopID := middleware.GetShopID(c)
|
shopID := middleware.GetShopID(c)
|
||||||
result := h.db.Model(&model.StockInOrder{}).
|
userID := middleware.GetUserID(c)
|
||||||
Where("id = ? AND shop_id = ? AND status = 'pending'", c.Param("id"), shopID).
|
role := middleware.GetRole(c)
|
||||||
Update("status", "draft")
|
|
||||||
if result.RowsAffected == 0 {
|
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"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in pending status"})
|
||||||
return
|
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"})
|
c.JSON(http.StatusOK, gin.H{"message": "withdrawn"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,11 +204,11 @@ func TestStockInHandler_Withdraw(t *testing.T) {
|
|||||||
orderID := extractID(w)
|
orderID := extractID(w)
|
||||||
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), adminToken, nil)
|
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)
|
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/withdraw", orderID), opToken, nil)
|
||||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
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)
|
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/withdraw", orderID), adminToken, nil)
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-in/orders/%d", orderID), adminToken, nil)
|
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 后可再次修改并提交
|
// 4. 撤回为 draft 后可再次修改并提交
|
||||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), adminToken, nil)
|
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), adminToken, nil)
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
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) {
|
func TestStockInHandler_GetNotFound(t *testing.T) {
|
||||||
|
|||||||
@@ -242,16 +242,29 @@ func (h *StockOutHandler) Reject(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Withdraw PUT /api/v1/stock-out/orders/:id/withdraw
|
// Withdraw PUT /api/v1/stock-out/orders/:id/withdraw
|
||||||
// 审核中(pending)撤回为草稿(draft),供管理员修改后重新提交。仅 pending 可撤回;
|
// 审核中(pending)撤回为草稿(draft),修改后可重新提交。仅 pending 可撤回;
|
||||||
// 已审核(approved)单据只读、库存已变动,不可撤回。
|
// 已审核(approved)单据只读、库存已变动,不可撤回。
|
||||||
|
// 权限:管理员/超管可撤回任意单;普通操作员只能撤回本人提交的单(operator_id 为本人)。
|
||||||
func (h *StockOutHandler) Withdraw(c *gin.Context) {
|
func (h *StockOutHandler) Withdraw(c *gin.Context) {
|
||||||
shopID := middleware.GetShopID(c)
|
shopID := middleware.GetShopID(c)
|
||||||
result := h.db.Model(&model.StockOutOrder{}).
|
userID := middleware.GetUserID(c)
|
||||||
Where("id = ? AND shop_id = ? AND status = 'pending'", c.Param("id"), shopID).
|
role := middleware.GetRole(c)
|
||||||
Update("status", "draft")
|
|
||||||
if result.RowsAffected == 0 {
|
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"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in pending status"})
|
||||||
return
|
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"})
|
c.JSON(http.StatusOK, gin.H{"message": "withdrawn"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func setupProtectedRouter(db *gorm.DB) *gin.Engine {
|
|||||||
stockIn.PUT("/orders/:id/submit", stockInH.Submit)
|
stockIn.PUT("/orders/:id/submit", stockInH.Submit)
|
||||||
stockIn.PUT("/orders/:id/approve", stockInH.Approve)
|
stockIn.PUT("/orders/:id/approve", stockInH.Approve)
|
||||||
stockIn.PUT("/orders/:id/reject", stockInH.Reject)
|
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")
|
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/submit", stockOutH.Submit)
|
||||||
stockOut.PUT("/orders/:id/approve", stockOutH.Approve)
|
stockOut.PUT("/orders/:id/approve", stockOutH.Approve)
|
||||||
stockOut.PUT("/orders/:id/reject", stockOutH.Reject)
|
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")
|
inv := api.Group("/inventory")
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ func Setup(r *gin.Engine, db *gorm.DB) {
|
|||||||
stockIn.PUT("/orders/:id/submit", stockInH.Submit)
|
stockIn.PUT("/orders/:id/submit", stockInH.Submit)
|
||||||
stockIn.PUT("/orders/:id/approve", stockInH.Approve)
|
stockIn.PUT("/orders/:id/approve", stockInH.Approve)
|
||||||
stockIn.PUT("/orders/:id/reject", stockInH.Reject)
|
stockIn.PUT("/orders/:id/reject", stockInH.Reject)
|
||||||
// 撤回(审核中→草稿)仅管理员
|
// 撤回(审核中→草稿):管理员/超管任意单,操作员限本人单(handler 内判权)
|
||||||
stockIn.PUT("/orders/:id/withdraw", middleware.AdminOnly(), stockInH.Withdraw)
|
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/submit", stockOutH.Submit)
|
||||||
stockOut.PUT("/orders/:id/approve", stockOutH.Approve)
|
stockOut.PUT("/orders/:id/approve", stockOutH.Approve)
|
||||||
stockOut.PUT("/orders/:id/reject", stockOutH.Reject)
|
stockOut.PUT("/orders/:id/reject", stockOutH.Reject)
|
||||||
// 撤回(审核中→草稿)仅管理员
|
// 撤回(审核中→草稿):管理员/超管任意单,操作员限本人单(handler 内判权)
|
||||||
stockOut.PUT("/orders/:id/withdraw", middleware.AdminOnly(), stockOutH.Withdraw)
|
stockOut.PUT("/orders/:id/withdraw", stockOutH.Withdraw)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 库存
|
// 库存
|
||||||
|
|||||||
Reference in New Issue
Block a user