package handler import ( "crypto/rand" "encoding/hex" "net/http" "time" "silk-server-go/internal/middleware" "silk-server-go/internal/model" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // RegisterBiosecurityRoutes 注册种源、消毒和二维码身份路由。 func RegisterBiosecurityRoutes(rg *gin.RouterGroup, db *gorm.DB) { read := middleware.RequirePermission(db, "biosecurity:read") write := middleware.RequirePermission(db, "biosecurity:write") rg.GET("/biosecurity/seed-sources", read, listSeedSources(db)) rg.POST("/biosecurity/seed-sources", write, createSeedSource(db)) rg.PATCH("/biosecurity/seed-sources/:id", write, updateSeedSource(db)) rg.GET("/biosecurity/disinfection-records", read, listDisinfectionRecords(db)) rg.POST("/biosecurity/disinfection-records", write, createDisinfectionRecord(db)) rg.PATCH("/biosecurity/disinfection-records/:id", write, updateDisinfectionRecord(db)) rg.POST("/biosecurity/qr", write, issueQR(db)) rg.POST("/biosecurity/qr/resolve", read, resolveQR(db)) } func listSeedSources(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { q := db.Model(&model.SeedSource{}) if batch := c.Query("batchId"); batch != "" { q = q.Where("batch_id = ?", batch) } var list []model.SeedSource q.Order("created_at DESC").Limit(200).Find(&list) c.JSON(http.StatusOK, list) } } func createSeedSource(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var body struct { BatchID *string `json:"batchId"` ParentID *string `json:"parentId"` Supplier string `json:"supplier"` SeedBatchNo string `json:"seedBatchNo"` QuarantineNo *string `json:"quarantineNo"` Variety *string `json:"variety"` CertificateURL *string `json:"certificateUrl"` EntryAt *time.Time `json:"entryAt"` Note *string `json:"note"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } for _, id := range []*string{body.BatchID, body.ParentID} { if id != nil && !isUUID(*id) { c.JSON(http.StatusBadRequest, gin.H{"error": "关联 ID 不是合法的 UUID"}) return } } source := model.SeedSource{ PublicID: randomPublicID(), BatchID: body.BatchID, ParentID: body.ParentID, Supplier: body.Supplier, SeedBatchNo: body.SeedBatchNo, QuarantineNo: body.QuarantineNo, Variety: body.Variety, CertificateURL: body.CertificateURL, EntryAt: body.EntryAt, Note: body.Note, CreatedBy: currentUserID(c), } if err := model.ValidateSeedSource(source); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if source.ParentID != nil { if err := model.SeedSourceCycleError(seedParentChain(db), source.ID, *source.ParentID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } if err := db.Create(&source).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "创建种源失败"}) return } linkSeedSourceToBatch(db, source) c.JSON(http.StatusCreated, source) } } func updateSeedSource(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var source model.SeedSource if db.Where("id = ?", c.Param("id")).First(&source).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "seed source not found"}) return } var body struct { BatchID *string `json:"batchId"` ParentID *string `json:"parentId"` Supplier *string `json:"supplier"` SeedBatchNo *string `json:"seedBatchNo"` QuarantineNo *string `json:"quarantineNo"` Variety *string `json:"variety"` CertificateURL *string `json:"certificateUrl"` EntryAt *time.Time `json:"entryAt"` Note *string `json:"note"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updates := map[string]interface{}{} if body.BatchID != nil { updates["batch_id"] = *body.BatchID } if body.ParentID != nil { if *body.ParentID != "" { if err := model.SeedSourceCycleError(seedParentChain(db), source.ID, *body.ParentID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updates["parent_id"] = *body.ParentID } else { updates["parent_id"] = nil } } if body.Supplier != nil { updates["supplier"] = *body.Supplier } if body.SeedBatchNo != nil { updates["seed_batch_no"] = *body.SeedBatchNo } if body.QuarantineNo != nil { updates["quarantine_no"] = *body.QuarantineNo } if body.Variety != nil { updates["variety"] = *body.Variety } if body.CertificateURL != nil { updates["certificate_url"] = *body.CertificateURL } if body.EntryAt != nil { updates["entry_at"] = *body.EntryAt } if body.Note != nil { updates["note"] = *body.Note } if len(updates) > 0 { if err := db.Model(&model.SeedSource{}).Where("id = ?", source.ID).Updates(updates).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "更新种源失败"}) return } } db.Where("id = ?", source.ID).First(&source) if body.BatchID != nil { linkSeedSourceToBatch(db, source) } c.JSON(http.StatusOK, source) } } func listDisinfectionRecords(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { q := db.Model(&model.DisinfectionRecord{}) if room := c.Query("roomId"); room != "" { q = q.Where("room_id = ?", room) } if batch := c.Query("batchId"); batch != "" { q = q.Where("batch_id = ?", batch) } if kind := c.Query("kind"); kind != "" { q = q.Where("kind = ?", kind) } var list []model.DisinfectionRecord q.Order("created_at DESC").Limit(200).Find(&list) c.JSON(http.StatusOK, list) } } func createDisinfectionRecord(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var body model.DisinfectionRecord if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } body.ID = "" body.CreatedBy = currentUserID(c) if err := model.ValidateDisinfectionRecord(body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } now := time.Now() if body.Kind == "plan" && body.PlannedAt == nil { body.PlannedAt = &now } if body.Kind == "execution" { body.ExecutorID = currentUserID(c) if body.ExecutedAt == nil { body.ExecutedAt = &now } } if body.RoomID != nil && !isUUID(*body.RoomID) { c.JSON(http.StatusBadRequest, gin.H{"error": "roomId 不是合法的 UUID"}) return } if body.BatchID != nil && !isUUID(*body.BatchID) { c.JSON(http.StatusBadRequest, gin.H{"error": "batchId 不是合法的 UUID"}) return } if err := db.Create(&body).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "创建消毒记录失败"}) return } c.JSON(http.StatusCreated, body) } } func updateDisinfectionRecord(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var record model.DisinfectionRecord if db.Where("id = ?", c.Param("id")).First(&record).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "disinfection record not found"}) return } var body struct { ExecutedAt *time.Time `json:"executedAt"` ReviewedAt *time.Time `json:"reviewedAt"` ReviewerID *string `json:"reviewerId"` PhotoURL *string `json:"photoUrl"` Note *string `json:"note"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updates := map[string]interface{}{} now := time.Now() if body.ExecutedAt != nil { updates["executed_at"] = now updates["executor_id"] = currentUserID(c) } if body.ReviewedAt != nil { updates["reviewed_at"] = *body.ReviewedAt } if body.ReviewerID != nil { updates["reviewer_id"] = *body.ReviewerID } if body.PhotoURL != nil { updates["photo_url"] = *body.PhotoURL } if body.Note != nil { updates["note"] = *body.Note } if len(updates) > 0 { if err := db.Model(&model.DisinfectionRecord{}).Where("id = ?", record.ID).Updates(updates).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "更新消毒记录失败"}) return } } db.Where("id = ?", record.ID).First(&record) c.JSON(http.StatusOK, record) } } func issueQR(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var body struct { EntityType string `json:"entityType"` EntityID string `json:"entityId"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if !model.ValidQRIdentityType(body.EntityType) || !isUUID(body.EntityID) { c.JSON(http.StatusBadRequest, gin.H{"error": "entityType/entityId 无效"}) return } if !entityExists(db, body.EntityType, body.EntityID) { c.JSON(http.StatusNotFound, gin.H{"error": "二维码关联实体不存在"}) return } var link model.IdentityLink if db.Where("entity_type = ? AND entity_id = ?", body.EntityType, body.EntityID).First(&link).Error != nil { link = model.IdentityLink{PublicID: randomPublicID(), EntityType: body.EntityType, EntityID: body.EntityID, Version: model.QRVersion} if err := db.Create(&link).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "创建二维码失败"}) return } } c.JSON(http.StatusOK, gin.H{ "publicId": link.PublicID, "entityType": link.EntityType, "entityId": link.EntityID, "payload": model.EncodeQRPayload(link.EntityType, link.PublicID, link.Version), }) } } func resolveQR(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var body struct { Payload string `json:"payload"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } entityType, publicID, version, err := model.ParseQRPayload(body.Payload) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var link model.IdentityLink if db.Where("public_id = ? AND entity_type = ?", publicID, entityType).First(&link).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "二维码身份不存在或已失效"}) return } if link.Version != version { c.JSON(http.StatusConflict, gin.H{"error": "二维码版本不匹配"}) return } summary, ok := entitySummary(db, link.EntityType, link.EntityID) if !ok { c.JSON(http.StatusNotFound, gin.H{"error": "二维码关联实体不存在"}) return } c.JSON(http.StatusOK, gin.H{ "entityType": link.EntityType, "publicId": link.PublicID, "entity": summary, }) } } func seedParentChain(db *gorm.DB) map[string]string { var rows []struct { ID string `gorm:"column:id"` ParentID *string `gorm:"column:parent_id"` } db.Table("seed_sources").Select("id, parent_id").Scan(&rows) chain := make(map[string]string, len(rows)) for _, row := range rows { if row.ParentID != nil { chain[row.ID] = *row.ParentID } } return chain } func linkSeedSourceToBatch(db *gorm.DB, source model.SeedSource) { if source.BatchID == nil { return } _ = db.Model(&model.Batch{}).Where("id = ?", *source.BatchID).Update("seed_source_id", source.ID).Error } func entityExists(db *gorm.DB, entityType, entityID string) bool { switch entityType { case "batch": var batch model.Batch return db.Where("id = ?", entityID).First(&batch).Error == nil case "tray": var tray model.Tray return db.Where("id = ?", entityID).First(&tray).Error == nil case "sample": var sample model.Sample return db.Where("id = ?", entityID).First(&sample).Error == nil default: return false } } func entitySummary(db *gorm.DB, entityType, entityID string) (map[string]interface{}, bool) { switch entityType { case "batch": var batch model.Batch if db.Where("id = ?", entityID).First(&batch).Error != nil { return nil, false } return gin.H{"id": batch.ID, "name": batch.Name, "roomId": batch.RoomID}, true case "tray": var tray model.Tray if db.Where("id = ?", entityID).First(&tray).Error != nil { return nil, false } return gin.H{"id": tray.ID, "name": tray.Name, "roomId": tray.RoomID}, true case "sample": var sample model.Sample if db.Where("id = ?", entityID).First(&sample).Error != nil { return nil, false } return gin.H{"id": sample.ID, "sampleNo": sample.SampleNo, "detectionTaskId": sample.DetectionTaskID, "state": sample.State}, true default: return nil, false } } func randomPublicID() string { b := make([]byte, 24) if _, err := rand.Read(b); err != nil { return hex.EncodeToString([]byte(time.Now().Format(time.RFC3339Nano))) } return hex.EncodeToString(b) }