feat(backend): 出入库汇总近30天滚动窗口 + 跨租户安全加固

- summaryBounds:本月/近30天滚动双口径(stock-in/out Summary ?window=rolling30)
- security(SEC-001):新增 ownership.go ensureShopRef 写入侧防线(stock-in/out/finance/
  盘点建单的 warehouse/partner/product 外键归属校验);读取侧全部 Preload 补
  shop_id 作用域,finance Summary JOIN 补租户条件;回归测试 CrossTenantRefs
- security(SEC-002):release 模式 JWT 密钥为空/默认值时拒绝启动
- gofmt 对齐若干 model/cmd 文件

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-03 09:57:52 +08:00
parent 69890b9935
commit ca7595b113
22 changed files with 423 additions and 225 deletions
+16 -3
View File
@@ -230,7 +230,8 @@ func (h *InventoryHandler) Logs(c *gin.Context) {
query.Count(&total)
logs := make([]model.InventoryLog, 0)
query.Preload("Product").Preload("Warehouse").Offset((page-1)*pageSize).Limit(pageSize).Order("id DESC").Find(&logs)
query.Preload("Product", "shop_id = ?", shopID).
Preload("Warehouse", "shop_id = ?", shopID).Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&logs)
c.JSON(http.StatusOK, gin.H{"data": logs, "total": total, "page": page, "page_size": pageSize})
}
@@ -250,6 +251,18 @@ func (h *InventoryHandler) CreateCheck(c *gin.Context) {
req.OperatorID = operatorID
req.Status = "draft"
// SEC-P01:外键归属校验——仓库与明细商品必须属于本店,拒绝跨租户脏引用
if err := ensureShopRef(h.db, "warehouses", req.WarehouseID, shopID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for i := range req.Items {
if err := ensureShopRef(h.db, "products", req.Items[i].ProductID, shopID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
}
// 自动填入系统库存数量(SUM 聚合)
for i := range req.Items {
req.Items[i].ShopID = shopID
@@ -274,7 +287,7 @@ func (h *InventoryHandler) CreateCheck(c *gin.Context) {
func (h *InventoryHandler) GetCheck(c *gin.Context) {
shopID := middleware.GetShopID(c)
var check model.InventoryCheck
if err := h.db.Preload("Items.Product").
if err := h.db.Preload("Items.Product", "shop_id = ?", shopID).
Where("id = ? AND shop_id = ?", c.Param("id"), shopID).
First(&check).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
@@ -289,7 +302,7 @@ func (h *InventoryHandler) CompleteCheck(c *gin.Context) {
checkID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var check model.InventoryCheck
if err := h.db.Preload("Items.Product").
if err := h.db.Preload("Items.Product", "shop_id = ?", shopID).
Where("id = ? AND shop_id = ?", checkID, shopID).
First(&check).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})