feat: 补齐消毒、种源与二维码身份链
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user