feat(server-go): 耗材管理(库存/预警/采购建议,#16)

This commit is contained in:
weijuesen
2026-08-12 18:03:49 +08:00
parent 6ada0c74c5
commit 65c993abb5
6 changed files with 244 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
package model
import (
"math"
"time"
)
// Consumable 耗材
type Consumable struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
Name string `gorm:"size:64" json:"name"`
Category string `gorm:"size:32;index" json:"category"` // lamp_reagent/lamp_consumable/disinfectant/other
Spec *string `gorm:"size:128" json:"spec,omitempty"`
Quantity float64 `gorm:"type:float" json:"quantity"`
Unit *string `gorm:"size:32" json:"unit,omitempty"`
MinQuantity float64 `gorm:"column:min_quantity;type:float" json:"minQuantity"`
ExpiryDate *time.Time `gorm:"column:expiry_date;type:timestamptz" json:"expiryDate,omitempty"`
Supplier *string `gorm:"size:128" json:"supplier,omitempty"`
Note *string `gorm:"type:text" json:"note,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
}
func (Consumable) TableName() string { return "consumables" }
// LowStockAlert 库存低于安全阈值
func (c Consumable) LowStockAlert() bool {
return c.Quantity < c.MinQuantity
}
// ExpiringAlert 效期在 days 天内到期(含已过期)
func (c Consumable) ExpiringAlert(days int) bool {
if c.ExpiryDate == nil {
return false
}
return !c.ExpiryDate.After(time.Now().Add(time.Duration(days) * 24 * time.Hour))
}
// PurchaseSuggestion 建议采购量(低于安全阈值时补足差额,向上取整)
func (c Consumable) PurchaseSuggestion() float64 {
if !c.LowStockAlert() {
return 0
}
return math.Ceil(c.MinQuantity - c.Quantity)
}
@@ -0,0 +1,44 @@
package model
import (
"testing"
"time"
)
func TestConsumableLowStockAlert(t *testing.T) {
c := Consumable{Quantity: 5, MinQuantity: 10}
if !c.LowStockAlert() {
t.Error("库存低于安全阈值应预警")
}
c2 := Consumable{Quantity: 10, MinQuantity: 10}
if c2.LowStockAlert() {
t.Error("库存等于安全阈值不应预警")
}
}
func TestConsumableExpiringAlert(t *testing.T) {
soon := time.Now().Add(10 * 24 * time.Hour)
if !(Consumable{ExpiryDate: &soon}).ExpiringAlert(30) {
t.Error("30 天内到期应预警")
}
past := time.Now().Add(-24 * time.Hour)
if !(Consumable{ExpiryDate: &past}).ExpiringAlert(30) {
t.Error("已过期应预警")
}
far := time.Now().Add(60 * 24 * time.Hour)
if (Consumable{ExpiryDate: &far}).ExpiringAlert(30) {
t.Error("60 天后到期不应预警(30 天窗口)")
}
if (Consumable{}).ExpiringAlert(30) {
t.Error("无效期不应预警")
}
}
func TestPurchaseSuggestion(t *testing.T) {
if s := (Consumable{Quantity: 3, MinQuantity: 10}).PurchaseSuggestion(); s != 7 {
t.Errorf("建议采购量 = %v, want 7", s)
}
if s := (Consumable{Quantity: 10, MinQuantity: 10}).PurchaseSuggestion(); s != 0 {
t.Errorf("不缺货建议采购量 = %v, want 0", s)
}
}
@@ -37,6 +37,8 @@ var AllPermissions = []PermissionDef{
{"weather:read", "天气查看", "查看天气与高发病天气预警"},
{"lamp:read", "LAMP 检测查看", "查看 LAMP 检测任务单与结果"},
{"lamp:write", "LAMP 检测管理", "新建、编辑、录入 LAMP 检测结果"},
{"consumable:read", "耗材查看", "查看耗材库存与预警"},
{"consumable:write", "耗材管理", "新增、编辑、删除耗材"},
{"user:manage", "用户管理", "管理用户、角色和权限"},
{"audit:read", "审计查看", "查看审计日志"},
}
@@ -53,6 +55,7 @@ var RolePermissionMap = map[string][]string{
"notification:read", "notification:write",
"weather:read",
"lamp:read", "lamp:write",
"consumable:read", "consumable:write",
"user:manage", "audit:read",
},
RoleOperator: {
@@ -65,6 +68,7 @@ var RolePermissionMap = map[string][]string{
"notification:read", "notification:write",
"weather:read",
"lamp:read", "lamp:write",
"consumable:read", "consumable:write",
},
RoleViewer: {
"dashboard:view", "room:read", "device:read",
@@ -75,6 +79,7 @@ var RolePermissionMap = map[string][]string{
"notification:read",
"weather:read",
"lamp:read",
"consumable:read",
},
RoleFarmer: {
"dashboard:view", "room:read", "device:read", "device:control",
@@ -85,5 +90,6 @@ var RolePermissionMap = map[string][]string{
"notification:read", "notification:write",
"weather:read",
"lamp:read",
"consumable:read",
},
}