feat(server-go): 蚕匾/批次/饲养记录 CRUD 与权限(#7)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
Reference in New Issue
Block a user