issue-18 优化代码(只保证编译通过没检查)
This commit is contained in:
		| @@ -117,10 +117,11 @@ type HeartbeatConfig struct { | ||||
|  | ||||
| // ChirpStackConfig 代表 ChirpStack API 配置 | ||||
| type ChirpStackConfig struct { | ||||
| 	APIHost    string `yaml:"api_host"` | ||||
| 	APIToken   string `yaml:"api_token"` | ||||
| 	FPort      int    `yaml:"fport"` | ||||
| 	APITimeout int    `yaml:"api_timeout"` | ||||
| 	APIHost                  string `yaml:"api_host"` | ||||
| 	APIToken                 string `yaml:"api_token"` | ||||
| 	FPort                    int    `yaml:"fport"` | ||||
| 	APITimeout               int    `yaml:"api_timeout"` | ||||
| 	CollectionRequestTimeout int    `yaml:"collection_request_timeout"` | ||||
| } | ||||
|  | ||||
| // TaskConfig 代表任务调度配置 | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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" | ||||
| } | ||||
| @@ -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" | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
							
								
								
									
										67
									
								
								internal/infra/repository/pending_collection_repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								internal/infra/repository/pending_collection_repository.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| package repository | ||||
|  | ||||
| import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| // PendingCollectionRepository 定义了与待采集请求相关的数据库操作接口。 | ||||
| type PendingCollectionRepository interface { | ||||
| 	// Create 创建一个新的待采集请求。 | ||||
| 	Create(req *models.PendingCollection) error | ||||
|  | ||||
| 	// FindByCorrelationID 根据关联ID查找一个待采集请求。 | ||||
| 	FindByCorrelationID(correlationID string) (*models.PendingCollection, error) | ||||
|  | ||||
| 	// UpdateStatusToFulfilled 将指定关联ID的请求状态更新为“已完成”。 | ||||
| 	UpdateStatusToFulfilled(correlationID string, fulfilledAt time.Time) error | ||||
|  | ||||
| 	// MarkAllPendingAsTimedOut 将所有“待处理”请求更新为“已超时”。 | ||||
| 	MarkAllPendingAsTimedOut() (int64, error) | ||||
| } | ||||
|  | ||||
| // gormPendingCollectionRepository 是 PendingCollectionRepository 的 GORM 实现。 | ||||
| type gormPendingCollectionRepository struct { | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| // NewGormPendingCollectionRepository 创建一个新的 PendingCollectionRepository GORM 实现实例。 | ||||
| func NewGormPendingCollectionRepository(db *gorm.DB) PendingCollectionRepository { | ||||
| 	return &gormPendingCollectionRepository{db: db} | ||||
| } | ||||
|  | ||||
| // Create 创建一个新的待采集请求。 | ||||
| func (r *gormPendingCollectionRepository) Create(req *models.PendingCollection) error { | ||||
| 	return r.db.Create(req).Error | ||||
| } | ||||
|  | ||||
| // FindByCorrelationID 根据关联ID查找一个待采集请求。 | ||||
| func (r *gormPendingCollectionRepository) FindByCorrelationID(correlationID string) (*models.PendingCollection, error) { | ||||
| 	var req models.PendingCollection | ||||
| 	if err := r.db.First(&req, "correlation_id = ?", correlationID).Error; err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &req, nil | ||||
| } | ||||
|  | ||||
| // UpdateStatusToFulfilled 将指定关联ID的请求状态更新为“已完成”。 | ||||
| func (r *gormPendingCollectionRepository) UpdateStatusToFulfilled(correlationID string, fulfilledAt time.Time) error { | ||||
| 	return r.db.Model(&models.PendingCollection{}). | ||||
| 		Where("correlation_id = ?", correlationID). | ||||
| 		Updates(map[string]interface{}{ | ||||
| 			"status":       models.PendingStatusFulfilled, | ||||
| 			"fulfilled_at": &fulfilledAt, | ||||
| 		}).Error | ||||
| } | ||||
|  | ||||
| // MarkAllPendingAsTimedOut 将所有状态为 'pending' 的记录更新为 'timed_out'。 | ||||
| // 返回被更新的记录数量和错误。 | ||||
| func (r *gormPendingCollectionRepository) MarkAllPendingAsTimedOut() (int64, error) { | ||||
| 	result := r.db.Model(&models.PendingCollection{}). | ||||
| 		Where("status = ?", models.PendingStatusPending). | ||||
| 		Update("status", models.PendingStatusTimedOut) | ||||
|  | ||||
| 	return result.RowsAffected, result.Error | ||||
| } | ||||
| @@ -1,12 +1,12 @@ | ||||
| package lora | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"time" | ||||
|  | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/config" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport/lora/chirp_stack_proto/client/device_service" | ||||
| 	"github.com/go-openapi/runtime" | ||||
| 	httptransport "github.com/go-openapi/runtime/client" | ||||
| @@ -20,19 +20,13 @@ type ChirpStackTransport struct { | ||||
| 	client   *client.ChirpStackRESTAPI | ||||
| 	authInfo runtime.ClientAuthInfoWriter | ||||
| 	config   config.ChirpStackConfig | ||||
|  | ||||
| 	deviceCommandLogRepo repository.DeviceCommandLogRepository | ||||
| 	deviceRepo           repository.DeviceRepository | ||||
|  | ||||
| 	logger *logs.Logger | ||||
| 	logger   *logs.Logger | ||||
| } | ||||
|  | ||||
| // NewChirpStackTransport 创建一个新的通信实例,用于与 ChirpStack 通信。 | ||||
| func NewChirpStackTransport( | ||||
| 	config config.ChirpStackConfig, | ||||
| 	logger *logs.Logger, | ||||
| 	deviceCommandLogRepo repository.DeviceCommandLogRepository, | ||||
| 	deviceRepo repository.DeviceRepository, | ||||
| ) *ChirpStackTransport { | ||||
| 	// 使用配置中的服务器地址创建一个 HTTP transport。 | ||||
| 	// 它会使用生成的客户端中定义的默认 base path 和 schemes。 | ||||
| @@ -45,16 +39,14 @@ func NewChirpStackTransport( | ||||
| 	authInfo := httptransport.APIKeyAuth("grpc-metadata-authorization", "header", config.GenerateAPIKey()) | ||||
|  | ||||
| 	return &ChirpStackTransport{ | ||||
| 		client:               apiClient, | ||||
| 		authInfo:             authInfo, | ||||
| 		config:               config, | ||||
| 		logger:               logger, | ||||
| 		deviceCommandLogRepo: deviceCommandLogRepo, | ||||
| 		deviceRepo:           deviceRepo, | ||||
| 		client:   apiClient, | ||||
| 		authInfo: authInfo, | ||||
| 		config:   config, | ||||
| 		logger:   logger, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *ChirpStackTransport) Send(address string, payload []byte) error { | ||||
| func (c *ChirpStackTransport) Send(address string, payload []byte) (*transport.SendResult, error) { | ||||
| 	// 1. 构建 API 请求体。 | ||||
| 	//    - Confirmed: true 表示确认消息, 设为false将不保证消息送达(但可以节约下行容量)。 | ||||
| 	//    - Data: 经过 Base64 编码的数据。 | ||||
| @@ -72,7 +64,7 @@ func (c *ChirpStackTransport) Send(address string, payload []byte) error { | ||||
| 	//    - WithQueueItemDevEui 指定目标设备的 EUI。 | ||||
| 	//    - WithBody 设置请求体。 | ||||
| 	params := device_service.NewDeviceServiceEnqueueParams(). | ||||
| 		WithTimeout(10 * time.Second). | ||||
| 		WithTimeout(5 * time.Second). // TODO 这里应该从配置文件里读 | ||||
| 		WithQueueItemDevEui(address). | ||||
| 		WithBody(body) | ||||
|  | ||||
| @@ -81,53 +73,23 @@ func (c *ChirpStackTransport) Send(address string, payload []byte) error { | ||||
| 	resp, err := c.client.DeviceService.DeviceServiceEnqueue(params, c.authInfo) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("设备 %s 调用ChirpStack Enqueue失败: %v", address, err) | ||||
| 		return err | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// 4. 成功发送后,尝试记录下行任务 | ||||
| 	messageID := "" | ||||
| 	if resp != nil && resp.Payload != nil && resp.Payload.ID != "" { // 根据实际结构,使用 resp.Payload.ID | ||||
| 		messageID = resp.Payload.ID | ||||
| 	} else { | ||||
| 		c.logger.Warnf("ChirpStack Enqueue 响应未包含 MessageID (ID),无法记录下行任务。设备: %s", address) | ||||
| 		// 即使无法获取 MessageID,也认为发送成功,因为 ChirpStack Enqueue 成功了 | ||||
| 		return nil | ||||
| 	if resp == nil || resp.Payload == nil || resp.Payload.ID == "" { | ||||
| 		// 这是一个需要明确处理的错误情况,因为调用方依赖 MessageID。 | ||||
| 		errMsg := "ChirpStack Enqueue 响应未包含 MessageID (ID)" | ||||
| 		c.logger.Errorf(errMsg) | ||||
| 		return nil, errors.New(errMsg) | ||||
| 	} | ||||
|  | ||||
| 	// 调用私有方法记录下行任务 | ||||
| 	if err := c.recordDownlinkTask(address, messageID); err != nil { | ||||
| 		// 记录失败不影响下行命令的发送成功 | ||||
| 		c.logger.Errorf("记录下行任务失败 (MessageID: %s, DevEui: %s): %v", messageID, address, err) | ||||
| 		return nil | ||||
| 	c.logger.Infof("成功将 payload 发送到设备 %s 的队列 (MessageID: %s)", address, resp.Payload.ID) | ||||
|  | ||||
| 	// 将 MessageID 包装在 SendResult 中返回 | ||||
| 	result := &transport.SendResult{ | ||||
| 		MessageID: resp.Payload.ID, | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("设备 %s 调用ChirpStack Enqueue成功,并创建下行任务记录 (MessageID: %s)", address, messageID) | ||||
| 	return result, nil | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // recordDownlinkTask 记录下行任务到数据库 | ||||
| func (c *ChirpStackTransport) recordDownlinkTask(devEui string, messageID string) error { | ||||
| 	// 获取区域主控的内部 DeviceID | ||||
| 	regionalController, err := c.deviceRepo.FindByDevEui(devEui) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("记录下行任务失败:无法通过 DevEui '%s' 找到区域主控设备: %v", devEui, err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// 创建 DeviceCommandLog | ||||
| 	record := &models.DeviceCommandLog{ | ||||
| 		MessageID:      messageID, | ||||
| 		DeviceID:       regionalController.ID, | ||||
| 		SentAt:         time.Now(), | ||||
| 		AcknowledgedAt: nil, // 初始状态为未确认 | ||||
| 	} | ||||
|  | ||||
| 	if err := c.deviceCommandLogRepo.Create(record); err != nil { | ||||
| 		c.logger.Errorf("创建下行任务记录失败 (MessageID: %s, DeviceID: %d): %v", messageID, regionalController.ID, err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("成功创建下行任务记录 (MessageID: %s, DeviceID: %d)", messageID, regionalController.ID) | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -3,5 +3,13 @@ package transport | ||||
| // Communicator 用于其他设备通信 | ||||
| type Communicator interface { | ||||
| 	// Send 用于发送一条单向数据(不等待回信) | ||||
| 	Send(address string, payload []byte) error | ||||
| 	// 成功时,它返回一个包含 MessageID 的 SendResult,以便调用方追踪。 | ||||
| 	Send(address string, payload []byte) (*SendResult, error) | ||||
| } | ||||
|  | ||||
| // SendResult 包含了 SendGo 方法成功执行后返回的结果。 | ||||
| type SendResult struct { | ||||
| 	// MessageID 是通信服务为此次发送分配的唯一标识符。 | ||||
| 	// 调用方需要保存此 ID,以便后续关联 ACK 等事件。 | ||||
| 	MessageID string | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user