Files
pig-farm-controller/internal/infra/models/execution.go
2025-09-28 01:10:04 +08:00

221 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
}
// --- 用户审计日志 ---
// TODO 这些变量放这个包合适吗?
// --- 审计日志状态常量 ---
type AuditStatus string
const (
AuditStatusSuccess AuditStatus = "success"
AuditStatusFailed AuditStatus = "failed"
)
// --- 审计日志相关上下文键 ---
type AuditContextKey string
const (
ContextAuditActionType AuditContextKey = "auditActionType"
ContextAuditTargetResource AuditContextKey = "auditTargetResource"
ContextAuditDescription AuditContextKey = "auditDescription"
ContextUserKey AuditContextKey = "user"
)
func (a AuditContextKey) String() string {
return string(a)
}
// 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 AuditStatus `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
}