diff --git a/internal/app/api/api.go b/internal/app/api/api.go index ecc7d37..ac46297 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -24,6 +24,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/app/service" "git.huangwc.com/pig/pig-farm-controller/internal/app/webhook" "git.huangwc.com/pig/pig-farm-controller/internal/domain/audit" + domain_device "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" "git.huangwc.com/pig/pig-farm-controller/internal/domain/task" "git.huangwc.com/pig/pig-farm-controller/internal/domain/token" "git.huangwc.com/pig/pig-farm-controller/internal/infra/config" @@ -60,13 +61,14 @@ func NewAPI(cfg config.ServerConfig, userRepo repository.UserRepository, deviceRepository repository.DeviceRepository, areaControllerRepository repository.AreaControllerRepository, - deviceTemplateRepository repository.DeviceTemplateRepository, // 添加设备模板仓库 + deviceTemplateRepository repository.DeviceTemplateRepository, planRepository repository.PlanRepository, pigFarmService service.PigFarmService, - pigBatchService service.PigBatchService, // 添加猪群服务 + pigBatchService service.PigBatchService, userActionLogRepository repository.UserActionLogRepository, tokenService token.TokenService, - auditService audit.Service, // 注入审计服务 + auditService audit.Service, + deviceService domain_device.Service, listenHandler webhook.ListenHandler, analysisTaskManager *task.AnalysisPlanTaskManager) *API { // 设置 Gin 模式,例如 gin.ReleaseMode (生产模式) 或 gin.DebugMode (开发模式) @@ -93,7 +95,7 @@ func NewAPI(cfg config.ServerConfig, // 在 NewAPI 中初始化用户控制器,并将其作为 API 结构体的成员 userController: user.NewController(userRepo, userActionLogRepository, logger, tokenService), // 在 NewAPI 中初始化设备控制器,并将其作为 API 结构体的成员 - deviceController: device.NewController(deviceRepository, areaControllerRepository, deviceTemplateRepository, logger), + deviceController: device.NewController(deviceRepository, areaControllerRepository, deviceTemplateRepository, deviceService, logger), // 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员 planController: plan.NewController(logger, planRepository, analysisTaskManager), // 在 NewAPI 中初始化猪场管理控制器 @@ -160,11 +162,12 @@ func (a *API) setupRoutes() { // 设备相关路由组 deviceGroup := authGroup.Group("/devices") { - deviceGroup.POST("", a.deviceController.CreateDevice) // 创建设备 - deviceGroup.GET("", a.deviceController.ListDevices) // 获取设备列表 - deviceGroup.GET("/:id", a.deviceController.GetDevice) // 获取单个设备 - deviceGroup.PUT("/:id", a.deviceController.UpdateDevice) // 更新设备 - deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice) // 删除设备 + deviceGroup.POST("", a.deviceController.CreateDevice) // 创建设备 + deviceGroup.GET("", a.deviceController.ListDevices) // 获取设备列表 + deviceGroup.GET("/:id", a.deviceController.GetDevice) // 获取单个设备 + deviceGroup.PUT("/:id", a.deviceController.UpdateDevice) // 更新设备 + deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice) // 删除设备 + deviceGroup.POST("/manual-control/:id", a.deviceController.ManualControl) // 手动控制设备 } a.logger.Info("设备相关接口注册成功 (需要认证和审计)") @@ -236,7 +239,7 @@ func (a *API) setupRoutes() { pigBatchGroup.DELETE("/:id", a.pigBatchController.DeletePigBatch) // 删除猪群 pigBatchGroup.POST("/assign-pens/:id", a.pigBatchController.AssignEmptyPensToBatch) // 为猪群分配空栏 pigBatchGroup.POST("/reclassify-pen/:fromBatchID", a.pigBatchController.ReclassifyPenToNewBatch) // 将猪栏划拨到新群 - pigBatchGroup.DELETE("/remove-pen/:penID/:batchID", a.pigBatchController.RemoveEmptyPenFromBatch) // 从猪群移除空栏 + penGroup.DELETE("/remove-pen/:penID/:batchID", a.pigBatchController.RemoveEmptyPenFromBatch) // 从猪群移除空栏 pigBatchGroup.POST("/move-pigs-into-pen/:id", a.pigBatchController.MovePigsIntoPen) // 将猪只从“虚拟库存”移入指定猪栏 pigBatchGroup.POST("/sell-pigs/:id", a.pigBatchController.SellPigs) // 处理卖猪业务 pigBatchGroup.POST("/buy-pigs/:id", a.pigBatchController.BuyPigs) // 处理买猪业务 diff --git a/internal/app/controller/device/device_controller.go b/internal/app/controller/device/device_controller.go index 7d49124..0bed6fc 100644 --- a/internal/app/controller/device/device_controller.go +++ b/internal/app/controller/device/device_controller.go @@ -8,6 +8,7 @@ import ( "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/domain/device" "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" @@ -19,7 +20,8 @@ import ( type Controller struct { deviceRepo repository.DeviceRepository areaControllerRepo repository.AreaControllerRepository - deviceTemplateRepo repository.DeviceTemplateRepository // 设备模板仓库 + deviceTemplateRepo repository.DeviceTemplateRepository + deviceService device.Service logger *logs.Logger } @@ -27,13 +29,15 @@ type Controller struct { func NewController( deviceRepo repository.DeviceRepository, areaControllerRepo repository.AreaControllerRepository, - deviceTemplateRepo repository.DeviceTemplateRepository, // 注入设备模板仓库 + deviceTemplateRepo repository.DeviceTemplateRepository, + deviceService device.Service, logger *logs.Logger, ) *Controller { return &Controller{ deviceRepo: deviceRepo, areaControllerRepo: areaControllerRepo, deviceTemplateRepo: deviceTemplateRepo, + deviceService: deviceService, logger: logger, } } @@ -298,6 +302,67 @@ func (c *Controller) DeleteDevice(ctx *gin.Context) { controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备删除成功", nil, actionType, "设备删除成功", deviceID) } +// ManualControl godoc +// @Summary 手动控制设备 +// @Description 根据设备ID和指定的动作(开启或关闭)来手动控制设备 +// @Tags 设备管理 +// @Accept json +// @Produce json +// @Param id path string true "设备ID" +// @Param manualControl body dto.ManualControlDeviceRequest true "手动控制指令" +// @Success 200 {object} controller.Response +// @Router /api/v1/devices/manual-control/{id} [post] +func (c *Controller) ManualControl(ctx *gin.Context) { + const actionType = "手动控制设备" + deviceID := ctx.Param("id") + + var req dto.ManualControlDeviceRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) + return + } + + dev, err := c.deviceRepo.FindByIDString(deviceID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID) + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID) + return + } + if strings.Contains(err.Error(), "无效的设备ID格式") { + c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID) + return + } + c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "手动控制失败: "+err.Error(), actionType, "数据库查询失败", deviceID) + return + } + + c.logger.Infof("%s: 接收到指令, 设备ID: %s, 动作: %s", actionType, deviceID, req.Action) + if req.Action == nil { + err = c.deviceService.Collect(dev.AreaControllerID, []*models.Device{dev}) + if err != nil { + c.logger.Errorf("%s: 获取设备状态失败: %v, 设备ID: %s", actionType, err, deviceID) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备状态失败: "+err.Error(), actionType, "获取设备状态失败", deviceID) + } + } else { + action := device.DeviceActionStart + if *req.Action == "off" { + action = device.DeviceActionStop + } + err = c.deviceService.Switch(dev, action) + if err != nil { + c.logger.Errorf("%s: 设备控制失败: %v, 设备ID: %s", actionType, err, deviceID) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备控制失败: "+err.Error(), actionType, "设备控制失败", deviceID) + return + } + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "指令已发送", map[string]interface{}{"device_id": deviceID}, actionType, "指令发送成功", gin.H{"device_id": deviceID, "action": req.Action}) +} + // --- Controller Methods: Area Controllers --- // CreateAreaController godoc diff --git a/internal/app/dto/device_dto.go b/internal/app/dto/device_dto.go index d37f3f6..68a27ba 100644 --- a/internal/app/dto/device_dto.go +++ b/internal/app/dto/device_dto.go @@ -20,6 +20,12 @@ type UpdateDeviceRequest struct { Properties map[string]interface{} `json:"properties,omitempty"` } +// ManualControlDeviceRequest 定义了手动控制设备时需要传入的参数 +type ManualControlDeviceRequest struct { + // Action 不传表示这是一个传感器, 会触发一次采集 + Action *string `json:"action" binding:"oneof=on off"` +} + // CreateAreaControllerRequest 定义了创建区域主控时需要传入的参数 type CreateAreaControllerRequest struct { Name string `json:"name" binding:"required"` diff --git a/internal/core/application.go b/internal/core/application.go index 3658090..f123a78 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -163,6 +163,7 @@ func NewApplication(configPath string) (*Application, error) { userActionLogRepo, tokenService, auditService, + generalDeviceService, listenHandler, analysisPlanTaskManager, )