414 lines
13 KiB
Go
414 lines
13 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{})
|
|
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)
|
|
}
|