issue-18 优化代码(只保证编译通过没检查)

This commit is contained in:
2025-09-26 22:50:08 +08:00
parent d9fe1683d2
commit 23b7f66d74
17 changed files with 767 additions and 251 deletions

View File

@@ -87,6 +87,10 @@ type Device struct {
// 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"`
@@ -114,26 +118,48 @@ func (d *Device) ParseProperties(v interface{}) error {
// 方法会根据自身类型进行参数检查, 参数不全时返回false
// TODO 没写单测
func (d *Device) SelfCheck() bool {
properties := make(map[string]interface{})
if err := d.ParseProperties(&properties); err != nil {
return false
}
has := func(key string) bool {
_, ok := properties[key]
return ok
}
switch d.SubType {
case SubTypeFan:
if !has(BusNumber) || !has(BusAddress) || !has(RelayChannel) {
// 使用清晰的 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
}
// 检查通用属性是否存在
has := func(key string) bool {
_, ok := props[key]
return ok
}
// 根据子类型进行具体校验
switch d.SubType {
// 所有传感器类型都必须有 Command 和总线信息
case SubTypeSensorTemp, SubTypeSensorHumidity, SubTypeSensorWeight, SubTypeSensorAmmonia:
return d.Command != "" && has(BusNumber) && has(BusAddress)
// 所有开关类型都必须有继电器和总线信息
case SubTypeFan, SubTypeWaterCurtain, SubTypeValveFeed:
return has(BusNumber) && has(BusAddress) && has(RelayChannel)
// 如果是未知的子类型,或者没有子类型,则认为自检失败
default:
return false
}
// 如果设备类型不是已知的任何一种,则自检失败
default:
// 不应该有类型未知的设备
return false
}
return true
}

View File

@@ -1,32 +0,0 @@
package models
import (
"time"
)
// DeviceCommandLog 记录下行任务的下发情况和设备确认状态
type DeviceCommandLog struct {
// MessageID 是下行消息的唯一标识符。
// 可以是 ChirpStack 的 DeduplicationID 或其他系统生成的ID。
MessageID string `gorm:"primaryKey" json:"message_id"`
// DeviceID 是接收此下行任务的设备的ID。
// 对于 LoRaWAN这通常是区域主控设备的ID。
DeviceID uint `gorm:"not null;index" json:"device_id"`
// SentAt 记录下行任务最初发送的时间。
SentAt time.Time `gorm:"not null" json:"sent_at"`
// AcknowledgedAt 记录设备确认收到下行消息的时间。
// 如果设备未确认,则为零值或 NULL。使用指针类型 *time.Time 允许 NULL 值。
AcknowledgedAt *time.Time `json:"acknowledged_at"`
// ReceivedSuccess 表示设备是否成功接收到下行消息。
// true 表示设备已确认收到false 表示设备未确认收到或下发失败。
ReceivedSuccess bool `gorm:"not null" json:"received_success"`
}
// TableName 自定义 GORM 使用的数据库表名
func (DeviceCommandLog) TableName() string {
return "device_command_log"
}

View File

@@ -73,3 +73,70 @@ func (log *TaskExecutionLog) AfterFind(tx *gorm.DB) (err error) {
}
return
}
// --- 指令与采集 ---
// PendingCollectionStatus 定义了待采集请求的状态
type PendingCollectionStatus string
const (
PendingStatusPending PendingCollectionStatus = "pending" // 请求已发送,等待设备响应
PendingStatusFulfilled PendingCollectionStatus = "fulfilled" // 已收到设备响应并成功处理
PendingStatusTimedOut PendingCollectionStatus = "timed_out" // 请求超时,未收到设备响应
)
// DeviceCommandLog 记录所有“发后即忘”的下行指令日志。
// 这张表主要用于追踪指令是否被网关成功发送 (ack)。
type DeviceCommandLog struct {
// MessageID 是下行消息的唯一标识符。
// 可以是 ChirpStack 的 DeduplicationID 或其他系统生成的ID。
MessageID string `gorm:"primaryKey" json:"message_id"`
// DeviceID 是接收此下行任务的设备的ID。
// 对于 LoRaWAN这通常是区域主控设备的ID。
DeviceID uint `gorm:"not null;index" json:"device_id"`
// SentAt 记录下行任务最初发送的时间。
SentAt time.Time `gorm:"not null" json:"sent_at"`
// AcknowledgedAt 记录设备确认收到下行消息的时间。
// 如果设备未确认,则为零值或 NULL。使用指针类型 *time.Time 允许 NULL 值。
AcknowledgedAt *time.Time `json:"acknowledged_at"`
// ReceivedSuccess 表示设备是否成功接收到下行消息。
// true 表示设备已确认收到false 表示设备未确认收到或下发失败。
ReceivedSuccess bool `gorm:"not null" json:"received_success"`
}
// TableName 自定义 GORM 使用的数据库表名
func (DeviceCommandLog) TableName() string {
return "device_command_log"
}
// PendingCollection 记录所有需要设备响应的“待采集请求”。
// 这是一张状态机表,追踪从请求发送到收到响应的整个生命周期。
type PendingCollection struct {
// CorrelationID 是由平台生成的、在请求和响应之间全局唯一的关联ID作为主键。
CorrelationID string `gorm:"primaryKey"`
// DeviceID 是接收此任务的设备ID
// 对于 LoRaWAN这通常是区域主控设备的ID。
DeviceID uint `gorm:"index"`
// CommandMetadata 存储了此次采集任务对应的设备ID列表顺序与设备响应值的顺序一致。
CommandMetadata UintArray `gorm:"type:bigint[]"`
// Status 是该请求的当前状态,用于状态机管理和超时处理。
Status PendingCollectionStatus `gorm:"index"`
// FulfilledAt 是收到设备响应并成功处理的时间。使用指针以允许 NULL 值。
FulfilledAt *time.Time
// CreatedAt 是 GORM 的标准字段,记录请求创建时间。
CreatedAt time.Time
}
// TableName 自定义 GORM 使用的数据库表名
func (PendingCollection) TableName() string {
return "pending_collections"
}

View File

@@ -1,5 +1,13 @@
package models
import (
"database/sql/driver"
"errors"
"fmt"
"strconv"
"strings"
)
// GetAllModels 返回一个包含所有数据库模型实例的切片。
// 这个函数用于在数据库初始化时自动迁移所有的表结构。
func GetAllModels() []interface{} {
@@ -14,5 +22,70 @@ func GetAllModels() []interface{} {
&PendingTask{},
&SensorData{},
&DeviceCommandLog{},
&PendingCollection{},
}
}
// UintArray 是一个自定义类型,代表 uint 的切片。
// 它实现了 gorm.Scanner 和 driver.Valuer 接口,
// 以便能与数据库的 bigint[] 类型进行原生映射。
type UintArray []uint
// Value 实现了 driver.Valuer 接口。
// 它告诉 GORM 如何将 UintArray ([]) 转换为数据库能够理解的格式。
func (a UintArray) Value() (driver.Value, error) {
if a == nil {
return "{}", nil
}
var b strings.Builder
b.WriteString("{")
for i, v := range a {
if i > 0 {
b.WriteString(",")
}
b.WriteString(strconv.FormatUint(uint64(v), 10))
}
b.WriteString("}")
return b.String(), nil
}
// Scan 实现了 gorm.Scanner 接口。
// 它告诉 GORM 如何将从数据库读取的数据转换为我们的 UintArray ([])。
func (a *UintArray) Scan(src interface{}) error {
if src == nil {
*a = nil
return nil
}
var srcStr string
switch v := src.(type) {
case []byte:
srcStr = string(v)
case string:
srcStr = v
default:
return errors.New("无法扫描非字符串或字节类型的源到 UintArray")
}
// 去掉花括号
srcStr = strings.Trim(srcStr, "{}")
if srcStr == "" {
*a = []uint{}
return nil
}
// 按逗号分割
parts := strings.Split(srcStr, ",")
arr := make([]uint, len(parts))
for i, p := range parts {
val, err := strconv.ParseUint(p, 10, 64)
if err != nil {
return fmt.Errorf("解析 UintArray 元素失败: %w", err)
}
arr[i] = uint(val)
}
*a = arr
return nil
}