diff --git a/design/exceeding-threshold-alarm/index.md b/design/exceeding-threshold-alarm/index.md index 3b0fe71..b4ea6af 100644 --- a/design/exceeding-threshold-alarm/index.md +++ b/design/exceeding-threshold-alarm/index.md @@ -142,4 +142,5 @@ 10. 实现区域阈值告警任务 11. 实现区域阈值告警和设备阈值告警的增删改查 12. 实现任务11应的八个web接口 -13. 实现根据区域ID或设备ID清空对应阈值告警任务 \ No newline at end of file +13. 实现根据区域ID或设备ID清空对应阈值告警任务 +14. 设备和区域主控删除时清除对应区域阈值告警或设备阈值告警任务 \ No newline at end of file diff --git a/internal/app/service/device_service.go b/internal/app/service/device_service.go index 7b0af75..4a471c9 100644 --- a/internal/app/service/device_service.go +++ b/internal/app/service/device_service.go @@ -170,14 +170,8 @@ func (s *deviceService) DeleteDevice(ctx context.Context, id uint) error { return err // 如果未找到,会返回 gorm.ErrRecordNotFound } - // TODO 这个应该用事务处理 - err = s.thresholdAlarmService.DeleteDeviceThresholdAlarmByDeviceID(serviceCtx, id) - if err != nil { - return fmt.Errorf("删除设备阈值告警失败: %w", err) - } - // 在删除前检查设备是否被任务使用 - inUse, err := s.deviceRepo.IsDeviceInUse(serviceCtx, id) + inUse, err := s.deviceRepo.IsDeviceInUse(serviceCtx, id, []models.TaskType{models.TaskTypeDeviceThresholdCheck}) if err != nil { // 如果检查过程中发生数据库错误,则返回错误 return fmt.Errorf("检查设备使用情况失败: %w", err) @@ -187,6 +181,12 @@ func (s *deviceService) DeleteDevice(ctx context.Context, id uint) error { return ErrDeviceInUse } + // TODO 这个应该用事务处理 + err = s.thresholdAlarmService.DeleteDeviceThresholdAlarmByDeviceID(serviceCtx, id) + if err != nil { + return fmt.Errorf("删除设备阈值告警失败: %w", err) + } + // 只有在未被使用时,才执行删除操作 return s.deviceRepo.Delete(serviceCtx, id) } @@ -296,14 +296,8 @@ func (s *deviceService) DeleteAreaController(ctx context.Context, id uint) error return err // 如果未找到,gorm会返回 ErrRecordNotFound } - // TODO 这个应该用事务处理 - err = s.thresholdAlarmService.DeleteAreaThresholdAlarmByAreaControllerID(serviceCtx, id) - if err != nil { - return fmt.Errorf("删除区域阈值告警失败: %w", err) - } - // 2. 检查是否被使用(业务逻辑) - inUse, err := s.deviceRepo.IsAreaControllerInUse(serviceCtx, id) + inUse, err := s.areaControllerRepo.IsAreaControllerUsedByTasks(serviceCtx, id, []models.TaskType{models.TaskTypeAreaCollectorThresholdCheck}) if err != nil { return err // 返回数据库检查错误 } @@ -311,6 +305,12 @@ func (s *deviceService) DeleteAreaController(ctx context.Context, id uint) error return ErrAreaControllerInUse // 返回业务错误 } + // TODO 这个应该用事务处理 + err = s.thresholdAlarmService.DeleteAreaThresholdAlarmByAreaControllerID(serviceCtx, id) + if err != nil { + return fmt.Errorf("删除区域阈值告警失败: %w", err) + } + // 3. 执行删除 return s.areaControllerRepo.Delete(serviceCtx, id) } diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index f2b0560..6be7dc5 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -254,16 +254,6 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices infra.repos.pigTradeRepo, infra.repos.notificationRepo, ) - deviceService := service.NewDeviceService( - logs.AddCompName(baseCtx, "DeviceService"), - infra.repos.deviceRepo, - infra.repos.areaControllerRepo, - infra.repos.deviceTemplateRepo, - domainServices.generalDeviceService, - ) - auditService := service.NewAuditService(logs.AddCompName(baseCtx, "AuditService"), infra.repos.userActionLogRepo) - planService := service.NewPlanService(logs.AddCompName(baseCtx, "AppPlanService"), domainServices.planService) - userService := service.NewUserService(logs.AddCompName(baseCtx, "UserService"), infra.repos.userRepo, infra.tokenGenerator, domainServices.notifyService) // 初始化阈值告警服务 thresholdAlarmService := service.NewThresholdAlarmService( @@ -276,6 +266,19 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices infra.repos.deviceRepo, ) + deviceService := service.NewDeviceService( + logs.AddCompName(baseCtx, "DeviceService"), + infra.repos.deviceRepo, + infra.repos.areaControllerRepo, + infra.repos.deviceTemplateRepo, + domainServices.generalDeviceService, + thresholdAlarmService, + ) + + auditService := service.NewAuditService(logs.AddCompName(baseCtx, "AuditService"), infra.repos.userActionLogRepo) + planService := service.NewPlanService(logs.AddCompName(baseCtx, "AppPlanService"), domainServices.planService) + userService := service.NewUserService(logs.AddCompName(baseCtx, "UserService"), infra.repos.userRepo, infra.tokenGenerator, domainServices.notifyService) + return &AppServices{ pigFarmService: pigFarmService, pigBatchService: pigBatchService, diff --git a/internal/domain/task/task.go b/internal/domain/task/task.go index 7ee9f09..e8c51cf 100644 --- a/internal/domain/task/task.go +++ b/internal/domain/task/task.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/alarm" "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify" "git.huangwc.com/pig/pig-farm-controller/internal/domain/plan" @@ -28,6 +29,7 @@ type taskFactory struct { deviceService device.Service notificationService notify.Service + alarmService alarm.AlarmService } func NewTaskFactory( @@ -37,6 +39,7 @@ func NewTaskFactory( alarmRepo repository.AlarmRepository, deviceService device.Service, notifyService notify.Service, + alarmService alarm.AlarmService, ) plan.TaskFactory { return &taskFactory{ ctx: ctx, @@ -45,6 +48,7 @@ func NewTaskFactory( alarmRepo: alarmRepo, deviceService: deviceService, notificationService: notifyService, + alarmService: alarmService, } } @@ -60,6 +64,10 @@ func (t *taskFactory) Production(ctx context.Context, claimedLog *models.TaskExe return NewFullCollectionTask(logs.AddCompName(baseCtx, CompNameFullCollectionTask), claimedLog, t.deviceRepo, t.deviceService) case models.TaskTypeAlarmNotification: return NewAlarmNotificationTask(logs.AddCompName(baseCtx, CompNameAlarmNotification), claimedLog, t.notificationService, t.alarmRepo) + case models.TaskTypeDeviceThresholdCheck: + return NewDeviceThresholdCheckTask(logs.AddCompName(baseCtx, "DeviceThresholdCheckTask"), claimedLog, t.sensorDataRepo, t.alarmService) + case models.TaskTypeAreaCollectorThresholdCheck: + return NewAreaThresholdCheckTask(logs.AddCompName(baseCtx, "AreaCollectorThresholdCheckTask"), claimedLog, t.sensorDataRepo, t.deviceRepo, t.alarmService) default: // TODO 这里直接panic合适吗? 不过这个场景确实不该出现任何异常的任务类型 logger.Panicf("不支持的任务类型: %s", claimedLog.Task.Type) @@ -89,6 +97,10 @@ func (t *taskFactory) CreateTaskFromModel(ctx context.Context, taskModel *models return NewFullCollectionTask(logs.AddCompName(baseCtx, CompNameFullCollectionTask), tempLog, t.deviceRepo, t.deviceService), nil case models.TaskTypeAlarmNotification: return NewAlarmNotificationTask(logs.AddCompName(baseCtx, CompNameAlarmNotification), tempLog, t.notificationService, t.alarmRepo), nil + case models.TaskTypeDeviceThresholdCheck: + return NewDeviceThresholdCheckTask(logs.AddCompName(baseCtx, "DeviceThresholdCheckTask"), tempLog, t.sensorDataRepo, t.alarmService), nil + case models.TaskTypeAreaCollectorThresholdCheck: + return NewAreaThresholdCheckTask(logs.AddCompName(baseCtx, "AreaCollectorThresholdCheckTask"), tempLog, t.sensorDataRepo, t.deviceRepo, t.alarmService), nil default: return nil, fmt.Errorf("不支持为类型 '%s' 的任务创建模型实例", taskModel.Type) } diff --git a/internal/infra/repository/area_controller_repository.go b/internal/infra/repository/area_controller_repository.go index 4c0b62f..0a0e0bf 100644 --- a/internal/infra/repository/area_controller_repository.go +++ b/internal/infra/repository/area_controller_repository.go @@ -3,6 +3,7 @@ package repository import ( "context" "fmt" + "strconv" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" @@ -18,6 +19,8 @@ type AreaControllerRepository interface { ListAll(ctx context.Context) ([]*models.AreaController, error) Update(ctx context.Context, ac *models.AreaController) error Delete(ctx context.Context, id uint) error + // IsAreaControllerUsedByTasks 检查区域主控是否被特定任务类型使用,可以忽略指定任务类型 + IsAreaControllerUsedByTasks(ctx context.Context, areaControllerID uint, ignoredTaskTypes []models.TaskType) (bool, error) } // gormAreaControllerRepository 是 AreaControllerRepository 的 GORM 实现。 @@ -84,3 +87,66 @@ func (r *gormAreaControllerRepository) FindByNetworkID(ctx context.Context, netw } return &areaController, nil } + +// IsAreaControllerUsedByTasks 检查区域主控是否被特定任务类型使用,可以忽略指定任务类型 +func (r *gormAreaControllerRepository) IsAreaControllerUsedByTasks(ctx context.Context, areaControllerID uint, ignoredTaskTypes []models.TaskType) (bool, error) { + repoCtx, logger := logs.Trace(ctx, r.ctx, "IsAreaControllerUsedByTasks") + + // 将 ignoredTaskTypes 转换为 map,以便高效查找 + ignoredMap := make(map[models.TaskType]struct{}) + for _, tt := range ignoredTaskTypes { + ignoredMap[tt] = struct{}{} + } + + areaControllerIDStr := strconv.FormatUint(uint64(areaControllerID), 10) + + // 定义所有可能与 AreaControllerID 相关的任务类型列表 + // 方便未来扩展,如果新增任务类型与区域主控关联,只需在此处添加 + relevantTaskTypes := []models.TaskType{ + models.TaskTypeAreaCollectorThresholdCheck, + // TODO: 如果未来有其他任务类型通过 parameters 关联 AreaControllerID,请在此处添加 + // 例如: models.TaskTypeAnotherAreaControllerTask, + } + + for _, taskType := range relevantTaskTypes { + // 如果当前任务类型在忽略列表中,则跳过检查 + if _, ok := ignoredMap[taskType]; ok { + continue + } + + var taskCount int64 + var query *gorm.DB + + // 根据任务类型构建不同的查询条件 + switch taskType { + case models.TaskTypeAreaCollectorThresholdCheck: + // TaskTypeAreaCollectorThresholdCheck 任务的 AreaControllerID 存储在 parameters->>'AreaControllerID' + query = r.db.WithContext(repoCtx). + Model(&models.Task{}). + Where("type = ?", models.TaskTypeAreaCollectorThresholdCheck). + Where("parameters->>'AreaControllerID' = ?", areaControllerIDStr) + // TODO: 如果未来有其他任务类型通过不同的 parameters 字段关联 AreaControllerID,请在此处添加 case + // case models.TaskTypeAnotherAreaControllerTask: + // query = r.db.WithContext(repoCtx). + // Model(&models.Task{}). + // Where("type = ?", models.TaskTypeAnotherAreaControllerTask). + // Where("parameters->>'AnotherFieldForAreaControllerID' = ?", areaControllerIDStr) + default: + // 对于未明确处理的 relevantTaskTypes,可以记录警告或直接跳过 + logger.Warnf(fmt.Sprintf("IsAreaControllerUsedByTasks: 未处理的区域主控相关任务类型: %s", taskType)) + continue + } + + if query != nil { + err := query.Count(&taskCount).Error + if err != nil { + return false, fmt.Errorf("查询区域主控任务使用情况失败 (任务类型: %s): %w", taskType, err) + } + if taskCount > 0 { + return true, nil // 发现有未被忽略的任务正在使用此区域主控 + } + } + } + + return false, nil // 没有发现任何未被忽略的任务正在使用此区域主控 +} diff --git a/internal/infra/repository/device_repository.go b/internal/infra/repository/device_repository.go index 32757e3..e19b974 100644 --- a/internal/infra/repository/device_repository.go +++ b/internal/infra/repository/device_repository.go @@ -47,8 +47,8 @@ type DeviceRepository interface { // GetDevicesByIDsTx 在指定事务中根据ID列表获取设备 GetDevicesByIDsTx(ctx context.Context, tx *gorm.DB, ids []uint) ([]models.Device, error) - // IsDeviceInUse 检查设备是否被任何任务使用 - IsDeviceInUse(ctx context.Context, deviceID uint) (bool, error) + // IsDeviceInUse 检查设备是否被任何任务使用,可以忽略指定任务类型 + IsDeviceInUse(ctx context.Context, deviceID uint, ignoredTaskTypes []models.TaskType) (bool, error) // IsAreaControllerInUse 检查区域主控是否被任何设备使用 IsAreaControllerInUse(ctx context.Context, areaControllerID uint) (bool, error) @@ -184,12 +184,22 @@ func (r *gormDeviceRepository) FindByAreaControllerAndPhysicalAddress(ctx contex return &device, nil } -// IsDeviceInUse 检查设备是否被任何任务使用 -func (r *gormDeviceRepository) IsDeviceInUse(ctx context.Context, deviceID uint) (bool, error) { +// IsDeviceInUse 检查设备是否被任何任务使用,可以忽略指定任务类型 +func (r *gormDeviceRepository) IsDeviceInUse(ctx context.Context, deviceID uint, ignoredTaskTypes []models.TaskType) (bool, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "IsDeviceInUse") var count int64 - // 直接对 device_tasks 关联表进行 COUNT 操作,性能最高 - err := r.db.WithContext(repoCtx).Model(&models.DeviceTask{}).Where("device_id = ?", deviceID).Count(&count).Error + + // 构建查询,需要 JOIN tasks 表来过滤 TaskType + query := r.db.WithContext(repoCtx). + Model(&models.DeviceTask{}). + Joins("JOIN tasks ON tasks.id = device_tasks.task_id"). + Where("device_tasks.device_id = ?", deviceID) + + if len(ignoredTaskTypes) > 0 { + query = query.Where("tasks.type NOT IN (?)", ignoredTaskTypes) + } + + err := query.Count(&count).Error if err != nil { return false, fmt.Errorf("查询设备任务关联失败: %w", err) }