Files
silk/server-go/internal/handler/biosecurity.go
T
2026-08-17 21:43:26 +08:00

447 lines
14 KiB
Go

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{}).
Joins("LEFT JOIN batches b ON b.id = seed_sources.batch_id")
q = applyRoomScope(q, c, "b.room_id")
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
}
}
if body.BatchID != nil && !requireObjectAccess(c, db, "batch", *body.BatchID) {
return
}
if body.ParentID != nil && !requireObjectAccess(c, db, "seed_source", *body.ParentID) {
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) {
if !requireObjectAccess(c, db, "seed_source", c.Param("id")) {
return
}
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{}).
Joins("LEFT JOIN rooms r ON r.id = disinfection_records.room_id").
Joins("LEFT JOIN batches b ON b.id = disinfection_records.batch_id")
q = applyRoomScope(q, c, "COALESCE(r.id, b.room_id)")
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 body.RoomID != nil && !canAccessRoom(db, c, body.RoomID) {
c.JSON(http.StatusForbidden, gin.H{"error": "无权在该蚕房下创建消毒记录"})
return
}
if body.BatchID != nil && !requireObjectAccess(c, db, "batch", *body.BatchID) {
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) {
if !requireObjectAccess(c, db, "disinfection_record", c.Param("id")) {
return
}
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 !requireObjectAccess(c, db, body.EntityType, body.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
}
if roomID, ok := objectRoomID(db, link.EntityType, link.EntityID); ok {
if !canAccessRoom(db, c, roomID) {
c.JSON(http.StatusForbidden, 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)
}