diff --git a/server-go/cmd/server/main.go b/server-go/cmd/server/main.go index 97f453d..bf3c603 100644 --- a/server-go/cmd/server/main.go +++ b/server-go/cmd/server/main.go @@ -109,6 +109,7 @@ func main() { handler.RegisterStorageRoutes(api, db) handler.RegisterKnowledgeRoutes(api, db, s3Svc, cfg.S3BucketImages) handler.RegisterInspectionRoutes(api, db, s3Svc, aiSvc, cfg.S3BucketImages) + handler.RegisterTrayBatchRoutes(api, db) // 启动后台设备状态同步(每 30 秒查询 WVP 设备在线状态) go startDeviceStatusSync(db, mediaSvc) diff --git a/server-go/internal/database/db.go b/server-go/internal/database/db.go index 6b09c76..5130800 100644 --- a/server-go/internal/database/db.go +++ b/server-go/internal/database/db.go @@ -29,6 +29,7 @@ func Init(cfg *config.Config) error { &model.Permission{}, &model.RolePermission{}, &model.Disease{}, &model.KnowledgeArticle{}, &model.InspectionRecord{}, + &model.Tray{}, &model.Batch{}, &model.RearingRecord{}, ); err != nil { slog.Warn("自动迁移有警告(可忽略)", "err", err) } diff --git a/server-go/internal/handler/tray_batch.go b/server-go/internal/handler/tray_batch.go new file mode 100644 index 0000000..75d0188 --- /dev/null +++ b/server-go/internal/handler/tray_batch.go @@ -0,0 +1,263 @@ +package handler + +import ( + "net/http" + "time" + + "silk-server-go/internal/middleware" + "silk-server-go/internal/model" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// RegisterTrayBatchRoutes 注册蚕匾/批次/饲养记录路由 +func RegisterTrayBatchRoutes(rg *gin.RouterGroup, db *gorm.DB) { + trayRead := middleware.RequirePermission(db, "tray:read") + trayWrite := middleware.RequirePermission(db, "tray:write") + batchRead := middleware.RequirePermission(db, "batch:read") + batchWrite := middleware.RequirePermission(db, "batch:write") + rearingRead := middleware.RequirePermission(db, "rearing:read") + rearingWrite := middleware.RequirePermission(db, "rearing:write") + + rg.GET("/trays", trayRead, listTrays(db)) + rg.POST("/trays", trayWrite, createTray(db)) + rg.PATCH("/trays/:id", trayWrite, updateTray(db)) + rg.DELETE("/trays/:id", trayWrite, deleteTray(db)) + + rg.GET("/batches", batchRead, listBatches(db)) + rg.POST("/batches", batchWrite, createBatch(db)) + rg.PATCH("/batches/:id", batchWrite, updateBatch(db)) + rg.DELETE("/batches/:id", batchWrite, deleteBatch(db)) + + rg.GET("/rearing-records", rearingRead, listRearingRecords(db)) + rg.POST("/rearing-records", rearingWrite, createRearingRecord(db)) + rg.PATCH("/rearing-records/:id", rearingWrite, updateRearingRecord(db)) + rg.DELETE("/rearing-records/:id", rearingWrite, deleteRearingRecord(db)) +} + +// ---------- 蚕匾 ---------- + +func listTrays(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + q := db.Model(&model.Tray{}) + if room := c.Query("roomId"); room != "" { + q = q.Where("room_id = ?", room) + } + if enabled := c.Query("enabled"); enabled != "" { + q = q.Where("enabled = ?", enabled == "true") + } + var list []model.Tray + q.Order("name ASC").Find(&list) + c.JSON(http.StatusOK, list) + } +} + +func createTray(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + var t model.Tray + if err := c.ShouldBindJSON(&t); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + t.ID = "" + if t.Name == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "蚕匾名称不能为空"}) + return + } + if !isUUID(t.RoomID) { + c.JSON(http.StatusBadRequest, gin.H{"error": "roomId 不是合法的 UUID"}) + return + } + if err := db.Create(&t).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"}) + return + } + c.JSON(http.StatusCreated, t) + } +} + +func updateTray(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + id := c.Param("id") + var t model.Tray + if db.Where("id = ?", id).First(&t).Error != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "tray not found"}) + return + } + updates, err := bindUpdates(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if len(updates) > 0 { + db.Model(&model.Tray{}).Where("id = ?", id).Updates(updates) + } + db.Where("id = ?", id).First(&t) + c.JSON(http.StatusOK, t) + } +} + +func deleteTray(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + id := c.Param("id") + var t model.Tray + if db.Where("id = ?", id).First(&t).Error != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "tray not found"}) + return + } + db.Where("id = ?", id).Delete(&model.Tray{}) + c.JSON(http.StatusOK, gin.H{"id": id}) + } +} + +// ---------- 批次 ---------- + +func listBatches(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + q := db.Model(&model.Batch{}) + if room := c.Query("roomId"); room != "" { + q = q.Where("room_id = ?", room) + } + if status := c.Query("status"); status != "" { + q = q.Where("status = ?", status) + } + var list []model.Batch + q.Order("created_at DESC").Find(&list) + c.JSON(http.StatusOK, list) + } +} + +func createBatch(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + var b model.Batch + if err := c.ShouldBindJSON(&b); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + b.ID = "" + if b.Name == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "批次名称不能为空"}) + return + } + if !isUUID(b.RoomID) { + c.JSON(http.StatusBadRequest, gin.H{"error": "roomId 不是合法的 UUID"}) + return + } + if b.Status == "" { + b.Status = "rearing" + } + if err := db.Create(&b).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"}) + return + } + c.JSON(http.StatusCreated, b) + } +} + +func updateBatch(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + id := c.Param("id") + var b model.Batch + if db.Where("id = ?", id).First(&b).Error != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "batch not found"}) + return + } + updates, err := bindUpdates(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if len(updates) > 0 { + db.Model(&model.Batch{}).Where("id = ?", id).Updates(updates) + } + db.Where("id = ?", id).First(&b) + c.JSON(http.StatusOK, b) + } +} + +func deleteBatch(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + id := c.Param("id") + var b model.Batch + if db.Where("id = ?", id).First(&b).Error != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "batch not found"}) + return + } + // 级联删除饲养记录 + db.Where("batch_id = ?", id).Delete(&model.RearingRecord{}) + db.Where("id = ?", id).Delete(&model.Batch{}) + c.JSON(http.StatusOK, gin.H{"id": id}) + } +} + +// ---------- 饲养记录 ---------- + +func listRearingRecords(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + q := db.Model(&model.RearingRecord{}) + if batch := c.Query("batchId"); batch != "" { + q = q.Where("batch_id = ?", batch) + } + var list []model.RearingRecord + q.Order("record_date DESC").Find(&list) + c.JSON(http.StatusOK, list) + } +} + +func createRearingRecord(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + var r model.RearingRecord + if err := c.ShouldBindJSON(&r); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + r.ID = "" + if !isUUID(r.BatchID) { + c.JSON(http.StatusBadRequest, gin.H{"error": "batchId 不是合法的 UUID"}) + return + } + if r.RecordDate.IsZero() { + r.RecordDate = time.Now() + } + if err := db.Create(&r).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"}) + return + } + c.JSON(http.StatusCreated, r) + } +} + +func updateRearingRecord(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + id := c.Param("id") + var r model.RearingRecord + if db.Where("id = ?", id).First(&r).Error != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "rearing record not found"}) + return + } + updates, err := bindUpdates(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if len(updates) > 0 { + db.Model(&model.RearingRecord{}).Where("id = ?", id).Updates(updates) + } + db.Where("id = ?", id).First(&r) + c.JSON(http.StatusOK, r) + } +} + +func deleteRearingRecord(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + id := c.Param("id") + var r model.RearingRecord + if db.Where("id = ?", id).First(&r).Error != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "rearing record not found"}) + return + } + db.Where("id = ?", id).Delete(&model.RearingRecord{}) + c.JSON(http.StatusOK, gin.H{"id": id}) + } +} diff --git a/server-go/internal/model/permission_seed.go b/server-go/internal/model/permission_seed.go index de27531..3b190e2 100644 --- a/server-go/internal/model/permission_seed.go +++ b/server-go/internal/model/permission_seed.go @@ -26,6 +26,12 @@ var AllPermissions = []PermissionDef{ {"knowledge:write", "知识库管理", "新增、编辑、删除知识库内容"}, {"inspection:create", "巡检创建", "发起 AI 拍照巡检"}, {"inspection:read", "巡检查看", "查看 AI 巡检记录"}, + {"tray:read", "蚕匾查看", "查看蚕匾"}, + {"tray:write", "蚕匾管理", "新增、编辑、删除蚕匾"}, + {"batch:read", "批次查看", "查看蚕种批次与饲养记录"}, + {"batch:write", "批次管理", "新增、编辑、删除批次"}, + {"rearing:read", "饲养记录查看", "查看饲养记录"}, + {"rearing:write", "饲养记录管理", "新增、编辑、删除饲养记录"}, {"user:manage", "用户管理", "管理用户、角色和权限"}, {"audit:read", "审计查看", "查看审计日志"}, } @@ -38,6 +44,7 @@ var RolePermissionMap = map[string][]string{ "video:read", "video:record", "energy:view", "log:read", "knowledge:read", "knowledge:write", "inspection:create", "inspection:read", + "tray:read", "tray:write", "batch:read", "batch:write", "rearing:read", "rearing:write", "user:manage", "audit:read", }, RoleOperator: { @@ -46,17 +53,20 @@ var RolePermissionMap = map[string][]string{ "video:read", "video:record", "energy:view", "log:read", "knowledge:read", "knowledge:write", "inspection:create", "inspection:read", + "tray:read", "tray:write", "batch:read", "batch:write", "rearing:read", "rearing:write", }, RoleViewer: { "dashboard:view", "room:read", "device:read", "threshold:read", "alarm:read", "video:read", "energy:view", "knowledge:read", "inspection:read", + "tray:read", "batch:read", "rearing:read", }, RoleFarmer: { "dashboard:view", "room:read", "device:read", "device:control", "alarm:read", "alarm:ack", "video:read", "energy:view", "knowledge:read", "inspection:create", "inspection:read", + "tray:read", "batch:read", "rearing:read", }, } diff --git a/server-go/internal/model/tray_batch.go b/server-go/internal/model/tray_batch.go new file mode 100644 index 0000000..11c550d --- /dev/null +++ b/server-go/internal/model/tray_batch.go @@ -0,0 +1,52 @@ +package model + +import "time" + +// Tray 蚕匾 +type Tray struct { + ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` + Name string `gorm:"size:64" json:"name"` + Code *string `gorm:"size:64" json:"code,omitempty"` + Position *string `gorm:"size:128" json:"position,omitempty"` + RoomID string `gorm:"column:room_id;type:uuid;index" json:"roomId"` + Enabled bool `gorm:"default:true" json:"enabled"` + CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"` + UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"` +} + +func (Tray) TableName() string { return "trays" } + +// Batch 蚕种批次 +type Batch struct { + ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` + RoomID string `gorm:"column:room_id;type:uuid;index" json:"roomId"` + Name string `gorm:"size:64" json:"name"` + Variety *string `gorm:"size:64" json:"variety,omitempty"` // 品种 + Source *string `gorm:"size:128" json:"source,omitempty"` // 蚕种来源/供应商 + SeedBatchNo *string `gorm:"column:seed_batch_no;size:64" json:"seedBatchNo,omitempty"` // 蚕种批次号 + QuarantineNo *string `gorm:"column:quarantine_no;size:64" json:"quarantineNo,omitempty"` // 检疫证明编号 + Instar *int `gorm:"type:int" json:"instar,omitempty"` // 龄期 1-5 + EnteredAt *time.Time `gorm:"column:entered_at;type:timestamptz" json:"enteredAt,omitempty"` // 入房时间 + MountAt *time.Time `gorm:"column:mount_at;type:timestamptz" json:"mountAt,omitempty"` // 上蔟时间 + Status string `gorm:"size:16;default:rearing" json:"status"` // rearing/mounted/finished + 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 (Batch) TableName() string { return "batches" } + +// RearingRecord 饲养记录 +type RearingRecord struct { + ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` + BatchID string `gorm:"column:batch_id;type:uuid;index" json:"batchId"` + RecordDate time.Time `gorm:"column:record_date;type:timestamptz" json:"recordDate"` + Instar *int `gorm:"type:int" json:"instar,omitempty"` + MulberrySource *string `gorm:"column:mulberry_source;size:128" json:"mulberrySource,omitempty"` // 桑叶来源 + Density *string `gorm:"size:64" json:"density,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 (RearingRecord) TableName() string { return "rearing_records" }