170 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			170 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package models
 | ||
| 
 | ||
| import (
 | ||
| 	"encoding/json"
 | ||
| 	"errors"
 | ||
| 
 | ||
| 	"gorm.io/datatypes"
 | ||
| 	"gorm.io/gorm"
 | ||
| 
 | ||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils"
 | ||
| )
 | ||
| 
 | ||
| // DeviceType 定义了设备的高级类别
 | ||
| type DeviceType string
 | ||
| 
 | ||
| const (
 | ||
| 	// DeviceTypeAreaController 区域主控,负责管理一个片区的设备
 | ||
| 	DeviceTypeAreaController DeviceType = "area_controller"
 | ||
| 	// DeviceTypeDevice 普通设备,如传感器、阀门等
 | ||
| 	DeviceTypeDevice DeviceType = "device"
 | ||
| )
 | ||
| 
 | ||
| // DeviceSubType 定义了普通设备的具体子类别
 | ||
| type DeviceSubType string
 | ||
| 
 | ||
| const (
 | ||
| 	// SubTypeNone 未指定或不适用的子类型
 | ||
| 	SubTypeNone DeviceSubType = ""
 | ||
| 	// SubTypeSensorTemp 温度传感器
 | ||
| 	SubTypeSensorTemp DeviceSubType = "temperature"
 | ||
| 	// SubTypeSensorHumidity 湿度传感器
 | ||
| 	SubTypeSensorHumidity DeviceSubType = "humidity"
 | ||
| 	// SubTypeSensorAmmonia 氨气传感器
 | ||
| 	SubTypeSensorAmmonia DeviceSubType = "ammonia"
 | ||
| 	// SubTypeSensorWeight 电子秤
 | ||
| 	SubTypeSensorWeight DeviceSubType = "weight"
 | ||
| 
 | ||
| 	// SubTypeValveFeed 下料阀门
 | ||
| 	SubTypeValveFeed DeviceSubType = "feed_valve"
 | ||
| 	// SubTypeFan 风机
 | ||
| 	SubTypeFan DeviceSubType = "fan"
 | ||
| 	// SubTypeWaterCurtain 水帘
 | ||
| 	SubTypeWaterCurtain DeviceSubType = "water_curtain"
 | ||
| )
 | ||
| 
 | ||
| // 设备属性名大全
 | ||
| var (
 | ||
| 
 | ||
| 	// 普通开关式设备
 | ||
| 	BusNumber    = "bus_number"    // 总线号
 | ||
| 	BusAddress   = "bus_address"   // 总线地址
 | ||
| 	RelayChannel = "relay_channel" // 继电器通道号
 | ||
| 
 | ||
| 	// 区域主控
 | ||
| 	LoRaAddress = "lora_address" // 区域主控 LoRa 地址, 如果使用LoRa网关也可能是LoRa网关记录的设备ID
 | ||
| )
 | ||
| 
 | ||
| // --- Properties 结构体定义 ---
 | ||
| 
 | ||
| // LoraProperties 定义了区域主控的特有属性
 | ||
| type LoraProperties struct {
 | ||
| 	LoraAddress string `json:"lora_address"` // LoRa 地址
 | ||
| }
 | ||
| 
 | ||
| // BusProperties 定义了总线设备的特有属性
 | ||
| type BusProperties struct {
 | ||
| 	BusID      int `json:"bus_id"`      // 485 总线号
 | ||
| 	BusAddress int `json:"bus_address"` // 485 总线地址
 | ||
| }
 | ||
| 
 | ||
| // Device 代表系统中的所有设备
 | ||
| type Device struct {
 | ||
| 	// gorm.Model 内嵌了标准模型字段 (ID, CreatedAt, UpdatedAt, DeletedAt)
 | ||
| 	gorm.Model
 | ||
| 
 | ||
| 	// Name 是设备的业务名称,应清晰可读,例如 "1号猪舍温度传感器" 或 "做料车间主控"
 | ||
| 	Name string `gorm:"not null" json:"name"`
 | ||
| 
 | ||
| 	// Type 是设备的高级类别,用于区分区域主控和普通设备。建立索引以优化按类型查询。
 | ||
| 	Type DeviceType `gorm:"not null;index" json:"type"`
 | ||
| 
 | ||
| 	// SubType 是设备的子类别,用于描述普通设备的具体功能,例如 "temperature", "fan" 等。建立索引以优化按子类型查询。
 | ||
| 	SubType DeviceSubType `gorm:"index" json:"sub_type"`
 | ||
| 
 | ||
| 	// ParentID 指向其父级设备的ID。对于顶层设备(如区域主控),此值为 NULL。
 | ||
| 	// 使用指针类型 *uint 来允许 NULL 值,从而清晰地表示“无父级”,避免了使用 0 作为魔术数字的歧义。建立索引以优化层级查询。
 | ||
| 	ParentID *uint `gorm:"index" json:"parent_id"`
 | ||
| 
 | ||
| 	// Location 描述了设备的物理安装位置,例如 "1号猪舍东侧",方便运维。建立索引以优化按位置查询。
 | ||
| 	Location string `gorm:"index" json:"location"`
 | ||
| 
 | ||
| 	// Command 存储了与设备交互所需的具体指令。
 | ||
| 	// 例如,对于传感器,这里存储 Modbus 采集指令;对于开关和区域主控,这里可以为空。
 | ||
| 	Command string `gorm:"type:varchar(255)" json:"command"`
 | ||
| 
 | ||
| 	// Properties 用于存储特定类型设备的独有属性,采用JSON格式。
 | ||
| 	// 建议在应用层为不同子类型的设备定义专用的属性结构体(如 LoraProperties, BusProperties),以保证数据一致性。
 | ||
| 	Properties datatypes.JSON `json:"properties"`
 | ||
| }
 | ||
| 
 | ||
| // TableName 自定义 GORM 使用的数据库表名
 | ||
| func (Device) TableName() string {
 | ||
| 	return "devices"
 | ||
| }
 | ||
| 
 | ||
| // ParseProperties 解析 JSON 属性到一个具体的结构体中。
 | ||
| // 调用方需要传入一个指向目标结构体实例的指针。
 | ||
| // 示例:
 | ||
| //
 | ||
| //	var props LoraProperties
 | ||
| //	if err := device.ParseProperties(&props); err != nil { ... }
 | ||
| func (d *Device) ParseProperties(v interface{}) error {
 | ||
| 	if d.Properties == nil {
 | ||
| 		return errors.New("设备属性为空,无法解析")
 | ||
| 	}
 | ||
| 	return json.Unmarshal(d.Properties, v)
 | ||
| }
 | ||
| 
 | ||
| // SelfCheck 进行参数自检, 返回检测结果
 | ||
| // 方法会根据自身类型进行参数检查, 参数不全时返回false
 | ||
| func (d *Device) SelfCheck() bool {
 | ||
| 	// 使用清晰的 switch 结构,确保所有情况都被覆盖
 | ||
| 	switch d.Type {
 | ||
| 	case DeviceTypeAreaController:
 | ||
| 		props := make(map[string]interface{})
 | ||
| 		if err := d.ParseProperties(&props); err != nil {
 | ||
| 			return false
 | ||
| 		}
 | ||
| 		_, ok := props[LoRaAddress].(string)
 | ||
| 		return ok
 | ||
| 
 | ||
| 	case DeviceTypeDevice:
 | ||
| 		// 所有普通设备都必须有父级
 | ||
| 		if d.ParentID == nil || *d.ParentID == 0 {
 | ||
| 			return false
 | ||
| 		}
 | ||
| 
 | ||
| 		props := make(map[string]interface{})
 | ||
| 		if err := d.ParseProperties(&props); err != nil {
 | ||
| 			return false
 | ||
| 		}
 | ||
| 
 | ||
| 		// hasPureNumeric 检查一个key是否存在于map中,并且其值是纯数字(整数或可解析为整数的字符串)
 | ||
| 		hasPureNumeric := func(key string) bool {
 | ||
| 			val, ok := props[key]
 | ||
| 			if !ok {
 | ||
| 				return false // Key不存在
 | ||
| 			}
 | ||
| 			return utils.IsPureNumeric(val)
 | ||
| 		}
 | ||
| 
 | ||
| 		// 根据子类型进行具体校验
 | ||
| 		switch d.SubType {
 | ||
| 		// 所有传感器类型都必须有 Command 和总线信息,且总线信息为纯数字
 | ||
| 		case SubTypeSensorTemp, SubTypeSensorHumidity, SubTypeSensorWeight, SubTypeSensorAmmonia:
 | ||
| 			return d.Command != "" && hasPureNumeric(BusNumber) && hasPureNumeric(BusAddress)
 | ||
| 		// 所有开关类型都必须有继电器和总线信息,且都为纯数字
 | ||
| 		case SubTypeFan, SubTypeWaterCurtain, SubTypeValveFeed:
 | ||
| 			return hasPureNumeric(BusNumber) && hasPureNumeric(BusAddress) && hasPureNumeric(RelayChannel)
 | ||
| 		// 如果是未知的子类型,或者没有子类型,则认为自检失败
 | ||
| 		default:
 | ||
| 			return false
 | ||
| 		}
 | ||
| 
 | ||
| 	// 如果设备类型不是已知的任何一种,则自检失败
 | ||
| 	default:
 | ||
| 		return false
 | ||
| 	}
 | ||
| }
 |