145 lines
5.9 KiB
Go
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
|
|
}
|