diff --git a/internal/app/controller/monitor/monitor_controller.go b/internal/app/controller/monitor/monitor_controller.go new file mode 100644 index 0000000..a2fbfaf --- /dev/null +++ b/internal/app/controller/monitor/monitor_controller.go @@ -0,0 +1,81 @@ +package monitor + +import ( + "errors" + + "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" + "git.huangwc.com/pig/pig-farm-controller/internal/app/dto" + "git.huangwc.com/pig/pig-farm-controller/internal/app/service" + "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" + "github.com/gin-gonic/gin" +) + +// Controller 监控控制器,封装了所有与数据监控相关的业务逻辑 +type Controller struct { + monitorService *service.MonitorService + logger *logs.Logger +} + +// NewController 创建一个新的监控控制器实例 +func NewController(monitorService *service.MonitorService, logger *logs.Logger) *Controller { + return &Controller{ + monitorService: monitorService, + logger: logger, + } +} + +// ListSensorData godoc +// @Summary 获取传感器数据列表 +// @Description 根据提供的过滤条件,分页获取传感器数据 +// @Tags 数据监控 +// @Security BearerAuth +// @Produce json +// @Param query query dto.ListSensorDataRequest true "查询参数" +// @Success 200 {object} controller.Response{data=dto.ListSensorDataResponse} +// @Router /api/v1/monitor/sensor-data [get] +func (c *Controller) ListSensorData(ctx *gin.Context) { + const actionType = "获取传感器数据列表" + + // --- 绑定并校验查询参数 --- + var req dto.ListSensorDataRequest + if err := ctx.ShouldBindQuery(&req); err != nil { + c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) + return + } + + // --- 将请求 DTO 转换为仓库层需要的 Options 结构体 --- + opts := repository.SensorDataListOptions{ + DeviceID: req.DeviceID, + OrderBy: req.OrderBy, + StartTime: req.StartTime, + EndTime: req.EndTime, + } + if req.SensorType != nil { + sensorType := models.SensorType(*req.SensorType) + opts.SensorType = &sensorType + } + + // --- 调用服务层 --- + data, total, err := c.monitorService.ListSensorData(opts, req.Page, req.PageSize) + if err != nil { + // --- 错误处理:区分参数错误和内部错误 --- + if errors.Is(err, repository.ErrInvalidPagination) { + c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req) + return + } + + // 其他未知错误按内部错误处理 + c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取传感器数据失败: "+err.Error(), actionType, "服务层查询失败", req) + return + } + + // --- 构建并发送成功响应 --- + resp := dto.NewListSensorDataResponse(data, total, req.Page, req.PageSize) + c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取传感器数据成功", resp, actionType, "获取传感器数据成功", req) +} diff --git a/internal/app/dto/monitor_dto.go b/internal/app/dto/monitor_dto.go new file mode 100644 index 0000000..33fe234 --- /dev/null +++ b/internal/app/dto/monitor_dto.go @@ -0,0 +1,65 @@ +package dto + +import ( + "encoding/json" + "time" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" +) + +// ListSensorDataRequest 定义了获取传感器数据列表的请求参数 +// 使用 form 标签以便 Gin 可以从查询字符串中绑定数据 +type ListSensorDataRequest struct { + Page int `form:"page,default=1"` + PageSize int `form:"pageSize,default=10"` + DeviceID *uint `form:"device_id"` + SensorType *string `form:"sensor_type"` + StartTime *time.Time `form:"start_time" time_format:"rfc3339"` + EndTime *time.Time `form:"end_time" time_format:"rfc3339"` + OrderBy string `form:"order_by"` +} + +// SensorDataDTO 是用于API响应的传感器数据结构 +type SensorDataDTO struct { + Time time.Time `json:"time"` + DeviceID uint `json:"device_id"` + RegionalControllerID uint `json:"regional_controller_id"` + SensorType models.SensorType `json:"sensor_type"` + Data json.RawMessage `json:"data"` // 使用 json.RawMessage 以便直接输出 JSON 内容 +} + +// PaginationDTO 定义了分页信息的标准结构 +type PaginationDTO struct { + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +// ListSensorDataResponse 是获取传感器数据列表的响应结构 +type ListSensorDataResponse struct { + List []SensorDataDTO `json:"list"` + Pagination PaginationDTO `json:"pagination"` +} + +// NewListSensorDataResponse 从模型数据创建列表响应 DTO +func NewListSensorDataResponse(data []models.SensorData, total int64, page, pageSize int) *ListSensorDataResponse { + dtos := make([]SensorDataDTO, len(data)) + for i, item := range data { + dtos[i] = SensorDataDTO{ + Time: item.Time, + DeviceID: item.DeviceID, + RegionalControllerID: item.RegionalControllerID, + SensorType: item.SensorType, + Data: json.RawMessage(item.Data), // gorm.datatypes.JSON 是 []byte, 可直接转换为 json.RawMessage + } + } + + return &ListSensorDataResponse{ + List: dtos, + Pagination: PaginationDTO{ + Total: total, + Page: page, + PageSize: pageSize, + }, + } +} diff --git a/internal/app/service/monitor_service.go b/internal/app/service/monitor_service.go new file mode 100644 index 0000000..a2f8332 --- /dev/null +++ b/internal/app/service/monitor_service.go @@ -0,0 +1,26 @@ +package service + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" +) + +// MonitorService 定义了监控相关的业务逻辑服务 +type MonitorService struct { + sensorDataRepo repository.SensorDataRepository + // 在这里可以添加其他超表模型的仓库依赖 +} + +// NewMonitorService 创建一个新的 MonitorService 实例 +func NewMonitorService(sensorDataRepo repository.SensorDataRepository) *MonitorService { + return &MonitorService{ + sensorDataRepo: sensorDataRepo, + } +} + +// ListSensorData 负责处理查询传感器数据列表的业务逻辑 +func (s *MonitorService) ListSensorData(opts repository.SensorDataListOptions, page, pageSize int) ([]models.SensorData, int64, error) { + // 目前业务逻辑只是简单地将请求透传给仓库层。 + // 未来任何与查询传感器数据相关的业务规则(如权限校验、数据聚合等)都应在此处实现。 + return s.sensorDataRepo.List(opts, page, pageSize) +} diff --git a/internal/infra/repository/sensor_data_repository.go b/internal/infra/repository/sensor_data_repository.go index 577ccb0..9212c7c 100644 --- a/internal/infra/repository/sensor_data_repository.go +++ b/internal/infra/repository/sensor_data_repository.go @@ -1,16 +1,31 @@ package repository import ( + "errors" "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "gorm.io/gorm" ) +// ErrInvalidPagination 表示分页参数无效 +var ErrInvalidPagination = errors.New("invalid pagination parameters: page and pageSize must be positive") + +// SensorDataListOptions 定义了查询传感器数据列表时的可选参数 +type SensorDataListOptions struct { + DeviceID *uint + SensorType *models.SensorType + StartTime *time.Time + EndTime *time.Time + OrderBy string // 例如 "time DESC" +} + // SensorDataRepository 定义了与传感器数据相关的数据库操作接口。 type SensorDataRepository interface { Create(sensorData *models.SensorData) error GetLatestSensorDataByDeviceIDAndSensorType(deviceID uint, sensorType models.SensorType) (*models.SensorData, error) + // List 支持分页和过滤的列表查询 + List(opts SensorDataListOptions, page, pageSize int) ([]models.SensorData, int64, error) } // gormSensorDataRepository 是 SensorDataRepository 的 GORM 实现。 @@ -19,7 +34,6 @@ type gormSensorDataRepository struct { } // NewGormSensorDataRepository 创建一个新的 SensorDataRepository GORM 实现实例。 -// 它直接接收一个 *gorm.DB 实例作为依赖,完全遵循项目中的既定模式。 func NewGormSensorDataRepository(db *gorm.DB) SensorDataRepository { return &gormSensorDataRepository{db: db} } @@ -38,3 +52,48 @@ func (r *gormSensorDataRepository) GetLatestSensorDataByDeviceIDAndSensorType(de First(&sensorData).Error return &sensorData, err } + +// List 实现了分页和过滤查询传感器数据的功能 +func (r *gormSensorDataRepository) List(opts SensorDataListOptions, page, pageSize int) ([]models.SensorData, int64, error) { + // --- 校验分页参数 --- + if page <= 0 || pageSize <= 0 { + return nil, 0, ErrInvalidPagination + } + + var results []models.SensorData + var total int64 + + query := r.db.Model(&models.SensorData{}) + + // --- 应用过滤条件 --- + if opts.DeviceID != nil { + query = query.Where("device_id = ?", *opts.DeviceID) + } + if opts.SensorType != nil { + query = query.Where("sensor_type = ?", *opts.SensorType) + } + if opts.StartTime != nil { + query = query.Where("time >= ?", *opts.StartTime) + } + if opts.EndTime != nil { + query = query.Where("time <= ?", *opts.EndTime) + } + + // --- 计算总数 --- + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // --- 应用排序条件 --- + orderBy := "time DESC" // 默认排序 + if opts.OrderBy != "" { + orderBy = opts.OrderBy + } + query = query.Order(orderBy) + + // --- 分页 --- + offset := (page - 1) * pageSize + err := query.Limit(pageSize).Offset(offset).Find(&results).Error + + return results, total, err +}