package models import ( "encoding/json" "errors" "time" "gorm.io/datatypes" "gorm.io/gorm" ) // 定义系统任务的特殊ID const ( SystemTaskIDResolvePlan int = -1 // 代表“解析计划”的系统任务 ) type ExecutionStatus string const ( ExecutionStatusStarted ExecutionStatus = "started" // 开始执行 ExecutionStatusCompleted ExecutionStatus = "completed" // 执行完成 ExecutionStatusFailed ExecutionStatus = "failed" // 执行失败 ExecutionStatusCancelled ExecutionStatus = "cancelled" // 执行取消 ExecutionStatusWaiting ExecutionStatus = "waiting" // 等待执行 (用于预写日志) ) // PlanExecutionLog 记录整个计划的一次执行历史 type PlanExecutionLog struct { gorm.Model PlanID uint `gorm:"index"` Status ExecutionStatus StartedAt time.Time EndedAt time.Time Error string } // TableName 自定义 GORM 使用的数据库表名 func (PlanExecutionLog) TableName() string { return "plan_execution_logs" } // TaskExecutionLog 记录单个任务的一次执行历史 type TaskExecutionLog struct { gorm.Model PlanExecutionLogID uint `gorm:"index"` // 关联到某次计划执行 // TaskID 使用 int 类型以容纳特殊的负数ID,代表系统任务 TaskID int `gorm:"index"` // 关键改动:移除了 OnDelete 约束。 Task Task `gorm:"foreignKey:TaskID;constraint:OnUpdate:CASCADE;"` Status ExecutionStatus Output string // 任务执行的输出或错误信息 StartedAt time.Time EndedAt time.Time } // TableName 自定义 GORM 使用的数据库表名 func (TaskExecutionLog) TableName() string { return "task_execution_logs" } // AfterFind 是 GORM 的一个钩子,在查询数据后自动执行 // 我们用它来优雅地处理系统任务的“虚拟”Task定义 func (log *TaskExecutionLog) AfterFind(tx *gorm.DB) (err error) { // 检查是否是我们的“解析计划”系统任务 if log.TaskID == SystemTaskIDResolvePlan { // 如果是,手动创建一个写死的 Task 定义并绑定上去 // 这使得上层服务在处理日志时,无需关心TaskID是否为负数 log.Task = Task{ // 注意:这里不能设置 ID,否则 GORM 可能会混淆 Name: "系统:解析并启动计划", Description: "这是一个由系统自动触发的内部任务,用于准备计划的执行。", } } 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" } // --- 用户审计日志 --- // UserActionLog 记录用户的操作历史,用于审计 type UserActionLog struct { // Time 是操作发生的时间,作为主键和超表的时间分区键 Time time.Time `gorm:"primaryKey" json:"time"` // --- Who (谁) --- UserID uint `gorm:"index" json:"user_id,omitempty"` Username string `json:"username,omitempty"` // 操作发生时用户名的快照 // --- Where (何地) --- SourceIP string `json:"source_ip,omitempty"` // --- What (什么) & How (如何) --- ActionType string `gorm:"index" json:"action_type,omitempty"` // 标准化的操作类型,如 "CREATE_DEVICE" TargetResource datatypes.JSON `gorm:"type:jsonb" json:"target_resource,omitempty"` // 被操作的资源, e.g., {"type": "device", "id": 123} Description string `json:"description,omitempty"` // 人类可读的操作描述 Status string `json:"status,omitempty"` // success 或 failed HTTPPath string `json:"http_path,omitempty"` // 请求的API路径 HTTPMethod string `json:"http_method,omitempty"` // 请求的HTTP方法 ResultDetails string `json:"result_details,omitempty"` // 结果详情,如失败时的错误信息 } // TableName 自定义 GORM 使用的数据库表名 func (UserActionLog) TableName() string { return "user_action_logs" } // ParseTargetResource 解析 JSON 属性到一个具体的结构体中。 // 调用方需要传入一个指向目标结构体实例的指针。 func (l *UserActionLog) ParseTargetResource(v interface{}) error { if l.TargetResource == nil { return errors.New("目标资源为空,无法解析") } return json.Unmarshal(l.TargetResource, v) } // SetTargetResource 将任意结构体序列化为 JSON 并设置到 TargetResource 字段 func (l *UserActionLog) SetTargetResource(data interface{}) error { if data == nil { l.TargetResource = nil return nil } bytes, err := json.Marshal(data) if err != nil { return err } l.TargetResource = bytes return nil }