Files
silk/server-go/internal/model/biosecurity.go
T
2026-08-14 08:39:56 +08:00

145 lines
5.9 KiB
Go

package model
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
)
const QRVersion = 1
// IdentityLink 不透明二维码映射,避免把内部 ID 或个人信息放进二维码。
type IdentityLink struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
PublicID string `gorm:"column:public_id;size:64;uniqueIndex" json:"publicId"`
EntityType string `gorm:"column:entity_type;size:16;index" json:"entityType"`
EntityID string `gorm:"column:entity_id;size:128;uniqueIndex" json:"entityId"`
Version int `gorm:"default:1" json:"version"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
}
func (IdentityLink) TableName() string { return "identity_links" }
// SeedSource 蚕种来源与检疫链。
type SeedSource struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
PublicID string `gorm:"column:public_id;size:64;uniqueIndex" json:"publicId"`
BatchID *string `gorm:"column:batch_id;type:uuid;index" json:"batchId,omitempty"`
ParentID *string `gorm:"column:parent_id;type:uuid;index" json:"parentId,omitempty"`
Supplier string `gorm:"size:128" json:"supplier"`
SeedBatchNo string `gorm:"column:seed_batch_no;size:64" json:"seedBatchNo"`
QuarantineNo *string `gorm:"column:quarantine_no;size:64" json:"quarantineNo,omitempty"`
Variety *string `gorm:"size:64" json:"variety,omitempty"`
CertificateURL *string `gorm:"column:certificate_url;size:512" json:"certificateUrl,omitempty"`
EntryAt *time.Time `gorm:"column:entry_at;type:timestamptz" json:"entryAt,omitempty"`
Note *string `gorm:"type:text" json:"note,omitempty"`
CreatedBy *string `gorm:"column:created_by;type:uuid" json:"createdBy,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
}
func (SeedSource) TableName() string { return "seed_sources" }
// DisinfectionRecord 消毒计划与执行记录。
type DisinfectionRecord struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
RoomID *string `gorm:"column:room_id;type:uuid;index" json:"roomId,omitempty"`
BatchID *string `gorm:"column:batch_id;type:uuid;index" json:"batchId,omitempty"`
Kind string `gorm:"size:16;default:plan" json:"kind"` // plan/execution
PlanID *string `gorm:"column:plan_id;type:uuid;index" json:"planId,omitempty"`
Agent string `gorm:"size:128" json:"agent"`
Concentration string `gorm:"size:64" json:"concentration"`
Amount *string `gorm:"size:64" json:"amount,omitempty"`
PlannedAt *time.Time `gorm:"column:planned_at;type:timestamptz" json:"plannedAt,omitempty"`
ExecutedAt *time.Time `gorm:"column:executed_at;type:timestamptz" json:"executedAt,omitempty"`
ExecutorID *string `gorm:"column:executor_id;type:uuid" json:"executorId,omitempty"`
ReviewedAt *time.Time `gorm:"column:reviewed_at;type:timestamptz" json:"reviewedAt,omitempty"`
ReviewerID *string `gorm:"column:reviewer_id;type:uuid" json:"reviewerId,omitempty"`
PhotoURL *string `gorm:"column:photo_url;size:512" json:"photoUrl,omitempty"`
Note *string `gorm:"type:text" json:"note,omitempty"`
CreatedBy *string `gorm:"column:created_by;type:uuid" json:"createdBy,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
}
func (DisinfectionRecord) TableName() string { return "disinfection_records" }
// EncodeQRPayload 生成不透明二维码载荷。
func EncodeQRPayload(entityType, publicID string, version int) string {
return fmt.Sprintf("silk:v1:%s:%s:%d", entityType, publicID, version)
}
// ParseQRPayload 解析并校验二维码载荷。
func ParseQRPayload(payload string) (string, string, int, error) {
parts := strings.Split(strings.TrimSpace(payload), ":")
if len(parts) != 5 || parts[0] != "silk" || parts[1] != "v1" {
return "", "", 0, errors.New("二维码格式不正确")
}
entityType := parts[2]
publicID := parts[3]
version, err := strconv.Atoi(parts[4])
if err != nil || version <= 0 {
return "", "", 0, errors.New("二维码版本不正确")
}
if !ValidQRIdentityType(entityType) || publicID == "" {
return "", "", 0, errors.New("二维码身份无效")
}
return entityType, publicID, version, nil
}
// ValidQRIdentityType 当前二维码支持的实体类型。
func ValidQRIdentityType(entityType string) bool {
switch entityType {
case "batch", "tray", "sample":
return true
default:
return false
}
}
// ValidateSeedSource 种源必填项。
func ValidateSeedSource(source SeedSource) error {
if strings.TrimSpace(source.Supplier) == "" {
return errors.New("供应商不能为空")
}
if strings.TrimSpace(source.SeedBatchNo) == "" {
return errors.New("蚕种批号不能为空")
}
return nil
}
// ValidateDisinfectionRecord 消毒必填项。
func ValidateDisinfectionRecord(record DisinfectionRecord) error {
if record.Kind != "plan" && record.Kind != "execution" {
return errors.New("kind 仅支持 plan/execution")
}
if strings.TrimSpace(record.Agent) == "" {
return errors.New("消毒药剂不能为空")
}
if strings.TrimSpace(record.Concentration) == "" {
return errors.New("消毒浓度不能为空")
}
return nil
}
// SeedSourceCycleError 检测种源链是否形成循环。
func SeedSourceCycleError(chain map[string]string, id, parentID string) error {
seen := map[string]bool{id: true}
current := parentID
for current != "" {
if seen[current] {
return errors.New("种源链不能形成循环")
}
seen[current] = true
next := chain[current]
if next == current {
return errors.New("种源链不能自引用")
}
current = next
}
return nil
}