From 9dc47ec7addcf376dbb019bab6dbf5737603e34f Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 10 Nov 2025 20:37:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=A0=B9=E6=8D=AE=E5=8C=BA?= =?UTF-8?q?=E5=9F=9FID=E6=88=96=E8=AE=BE=E5=A4=87ID=E6=B8=85=E7=A9=BA?= =?UTF-8?q?=E5=AF=B9=E5=BA=94=E9=98=88=E5=80=BC=E5=91=8A=E8=AD=A6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/exceeding-threshold-alarm/index.md | 3 +- internal/app/service/device_service.go | 35 +++-- .../app/service/threshold_alarm_service.go | 131 ++++++++++++++++++ internal/infra/repository/plan_repository.go | 18 +++ 4 files changed, 176 insertions(+), 11 deletions(-) diff --git a/design/exceeding-threshold-alarm/index.md b/design/exceeding-threshold-alarm/index.md index f9e25c8..3b0fe71 100644 --- a/design/exceeding-threshold-alarm/index.md +++ b/design/exceeding-threshold-alarm/index.md @@ -141,4 +141,5 @@ 9. 系统初始化时健康计划调整(包括增加延时任务) 10. 实现区域阈值告警任务 11. 实现区域阈值告警和设备阈值告警的增删改查 -12. 实现任务11应的八个web接口 \ No newline at end of file +12. 实现任务11应的八个web接口 +13. 实现根据区域ID或设备ID清空对应阈值告警任务 \ No newline at end of file diff --git a/internal/app/service/device_service.go b/internal/app/service/device_service.go index d00080e..7b0af75 100644 --- a/internal/app/service/device_service.go +++ b/internal/app/service/device_service.go @@ -49,11 +49,12 @@ type DeviceService interface { // deviceService 是 DeviceService 接口的具体实现。 type deviceService struct { - ctx context.Context - deviceRepo repository.DeviceRepository - areaControllerRepo repository.AreaControllerRepository - deviceTemplateRepo repository.DeviceTemplateRepository - deviceDomainSvc device.Service + ctx context.Context + deviceRepo repository.DeviceRepository + areaControllerRepo repository.AreaControllerRepository + deviceTemplateRepo repository.DeviceTemplateRepository + deviceDomainSvc device.Service + thresholdAlarmService ThresholdAlarmService } // NewDeviceService 创建一个新的 DeviceService 实例。 @@ -63,13 +64,15 @@ func NewDeviceService( areaControllerRepo repository.AreaControllerRepository, deviceTemplateRepo repository.DeviceTemplateRepository, deviceDomainSvc device.Service, + thresholdAlarmService ThresholdAlarmService, ) DeviceService { return &deviceService{ - ctx: ctx, - deviceRepo: deviceRepo, - areaControllerRepo: areaControllerRepo, - deviceTemplateRepo: deviceTemplateRepo, - deviceDomainSvc: deviceDomainSvc, + ctx: ctx, + deviceRepo: deviceRepo, + areaControllerRepo: areaControllerRepo, + deviceTemplateRepo: deviceTemplateRepo, + deviceDomainSvc: deviceDomainSvc, + thresholdAlarmService: thresholdAlarmService, } } @@ -167,6 +170,12 @@ 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) if err != nil { @@ -287,6 +296,12 @@ 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) if err != nil { diff --git a/internal/app/service/threshold_alarm_service.go b/internal/app/service/threshold_alarm_service.go index e954919..19a81ac 100644 --- a/internal/app/service/threshold_alarm_service.go +++ b/internal/app/service/threshold_alarm_service.go @@ -34,6 +34,8 @@ type ThresholdAlarmService interface { GetDeviceThresholdAlarm(ctx context.Context, taskID int) (*dto.DeviceThresholdAlarmDTO, error) // DeleteDeviceThresholdAlarm 删除一个设备阈值告警。 DeleteDeviceThresholdAlarm(ctx context.Context, taskID int, req *dto.DeleteDeviceThresholdAlarmDTO) error + // DeleteDeviceThresholdAlarmByDeviceID 实现了根据设备ID删除一个设备下所有设备阈值告警的逻辑。 + DeleteDeviceThresholdAlarmByDeviceID(ctx context.Context, deviceID uint) error // CreateAreaThresholdAlarm 创建一个区域阈值告警。 CreateAreaThresholdAlarm(ctx context.Context, req *dto.CreateAreaThresholdAlarmDTO) error @@ -43,6 +45,8 @@ type ThresholdAlarmService interface { GetAreaThresholdAlarm(ctx context.Context, taskID int) (*dto.AreaThresholdAlarmDTO, error) // DeleteAreaThresholdAlarm 实现了删除一个区域阈值告警的逻辑。 DeleteAreaThresholdAlarm(ctx context.Context, taskID int) error + // DeleteAreaThresholdAlarmByAreaControllerID 实现了根据区域ID删除一个区域下所有区域阈值告警的逻辑。 + DeleteAreaThresholdAlarmByAreaControllerID(ctx context.Context, areaControllerID uint) error } // thresholdAlarmService 是 ThresholdAlarmService 接口的具体实现。 @@ -370,6 +374,76 @@ func (s *thresholdAlarmService) DeleteDeviceThresholdAlarm(ctx context.Context, return err } +// DeleteDeviceThresholdAlarmByDeviceID 实现了根据设备ID删除一个设备下所有设备阈值告警的逻辑。 +func (s *thresholdAlarmService) DeleteDeviceThresholdAlarmByDeviceID(ctx context.Context, deviceID uint) error { + serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeleteDeviceThresholdAlarmByDeviceID") + tasks, err := s.planRepo.ListTasksByDeviceID(serviceCtx, deviceID) + if err != nil { + return fmt.Errorf("获取任务列表失败: %w", err) + } + device, err := s.deviceRepo.FindByID(serviceCtx, deviceID) + if err != nil { + return fmt.Errorf("获取设备 %d 失败: %w", deviceID, err) + } + plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck) + if err != nil { + return fmt.Errorf("获取系统计划失败: %w", err) + } + if plan == nil { + logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck) + } + + deleteNums := []int{} + for i, t := range plan.Tasks { + for _, dt := range tasks { + if t.ID == dt.ID && t.Type == models.TaskTypeDeviceThresholdCheck { + deleteNums = append(deleteNums, i) + break + } + } + + if t.Type == models.TaskTypeAreaCollectorThresholdCheck { + var areaParams task.AreaThresholdCheckParams + if err := t.ParseParameters(&areaParams); err != nil { + return fmt.Errorf("任务 %d: 解析参数失败: %w", t.ID, err) + } + if areaParams.AreaControllerID == device.AreaControllerID { + for ai, ae := range areaParams.ExcludeDeviceIDs { + if ae == deviceID { + areaParams.ExcludeDeviceIDs = append(areaParams.ExcludeDeviceIDs[:ai], areaParams.ExcludeDeviceIDs[ai+1:]...) + break + } + } + err = plan.Tasks[i].SaveParameters(areaParams) + if err != nil { + return fmt.Errorf("任务 %d: 保存参数失败: %w", t.ID, err) + } + } + } + } + + // 为了高效地判断一个索引是否需要被删除,先将 deleteNums 转换为一个 map。 + deleteMap := make(map[int]struct{}, len(deleteNums)) + for _, index := range deleteNums { + deleteMap[index] = struct{}{} + } + + // 创建一个新的任务切片,只包含不需要删除的任务。 + newTasks := make([]models.Task, 0, len(plan.Tasks)-len(deleteNums)) + for i, t := range plan.Tasks { + if _, found := deleteMap[i]; !found { + newTasks = append(newTasks, t) + } + } + plan.Tasks = newTasks + + // 重新排序任务并更新计划 + plan.ReorderSteps() + _, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem) + return err + +} + // CreateAreaThresholdAlarm 实现了创建一个区域阈值告警的逻辑。 func (s *thresholdAlarmService) CreateAreaThresholdAlarm(ctx context.Context, req *dto.CreateAreaThresholdAlarmDTO) error { serviceCtx, logger := logs.Trace(ctx, s.ctx, "CreateAreaThresholdAlarm") @@ -555,3 +629,60 @@ func (s *thresholdAlarmService) DeleteAreaThresholdAlarm(ctx context.Context, ta _, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem) return err } + +// DeleteAreaThresholdAlarmByAreaControllerID 实现了根据区域ID删除一个区域下所有区域阈值告警的逻辑。 +func (s *thresholdAlarmService) DeleteAreaThresholdAlarmByAreaControllerID(ctx context.Context, areaControllerID uint) error { + serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeleteAreaThresholdAlarmByAreaControllerID") + + // 1. 获取系统健康检查计划 + plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck) + if err != nil { + return fmt.Errorf("获取系统健康检查计划失败: %w", err) + } + if plan == nil { + logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck) + } + + // 2. 收集所有与指定 areaControllerID 相关的区域阈值告警任务的索引 + var deleteIndices []int + for i, t := range plan.Tasks { + // 只关心区域阈值检查任务 + if t.Type != models.TaskTypeAreaCollectorThresholdCheck { + continue + } + + var params task.AreaThresholdCheckParams + if err := t.ParseParameters(¶ms); err != nil { + return fmt.Errorf("任务 %d: 解析参数失败: %w", t.ID, err) + } + + // 如果 AreaControllerID 匹配,则记录其索引以待删除 + if params.AreaControllerID == areaControllerID { + deleteIndices = append(deleteIndices, i) + } + } + + // 如果没有找到要删除的任务,则直接返回 + if len(deleteIndices) == 0 { + return nil + } + + // 3. 使用 map 和新切片的方式安全地删除多个任务 + deleteMap := make(map[int]struct{}, len(deleteIndices)) + for _, index := range deleteIndices { + deleteMap[index] = struct{}{} + } + + newTasks := make([]models.Task, 0, len(plan.Tasks)-len(deleteMap)) + for i, t := range plan.Tasks { + if _, found := deleteMap[i]; !found { + newTasks = append(newTasks, t) + } + } + plan.Tasks = newTasks + + // 4. 重新排序任务并更新计划 + plan.ReorderSteps() + _, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem) + return err +} diff --git a/internal/infra/repository/plan_repository.go b/internal/infra/repository/plan_repository.go index e623fc0..38fe3f4 100644 --- a/internal/infra/repository/plan_repository.go +++ b/internal/infra/repository/plan_repository.go @@ -87,6 +87,8 @@ type PlanRepository interface { StopPlanTransactionally(ctx context.Context, planID uint) error // UpdatePlanStateAfterExecution 更新计划执行后的状态(计数和状态) UpdatePlanStateAfterExecution(ctx context.Context, planID uint, newCount uint, newStatus models.PlanStatus) error + // ListTasksByDeviceID 根据设备ID获取关联任务列表 + ListTasksByDeviceID(ctx context.Context, deviceID uint) ([]*models.Task, error) } // gormPlanRepository 是 PlanRepository 的 GORM 实现 @@ -878,3 +880,19 @@ func (r *gormPlanRepository) FindTaskByID(ctx context.Context, id int) (*models. } return &task, nil } + +func (r *gormPlanRepository) ListTasksByDeviceID(ctx context.Context, deviceID uint) ([]*models.Task, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "ListTasksByDeviceID") + tasks := []*models.Task{} + // 使用 Joins 方法来连接 tasks 表和 device_tasks 关联表, + // 然后通过 Where 子句筛选出与指定 deviceID 关联的所有任务。 + err := r.db.WithContext(repoCtx).Joins("JOIN device_tasks ON device_tasks.task_id = tasks.id"). + Where("device_tasks.device_id = ?", deviceID). + Find(&tasks).Error + + if err != nil { + return nil, err + } + + return tasks, nil +}