diff --git a/design/verification-before-device-deletion/check_before_device_deletion.md b/design/verification-before-device-deletion/check_before_device_deletion.md new file mode 100644 index 0000000..59d4a84 --- /dev/null +++ b/design/verification-before-device-deletion/check_before_device_deletion.md @@ -0,0 +1,41 @@ +# 方案:删除设备前的使用校验 + +## 1. 目标 + +在删除设备前,检查该设备是否被任何任务关联。如果设备正在被使用,则禁止删除,并向用户返回明确的错误提示。 + +## 2. 核心思路 + +我们将遵循您项目清晰的分层架构,将“检查设备是否被任务使用”这一业务规则放在 **应用层** (`internal/app/service/`) +中进行协调。当上层请求删除设备时,应用服务会先调用仓库层查询 `device_tasks` 关联表,如果发现设备仍被任务关联,则会拒绝删除并返回一个明确的业务错误。 + +## 3. 实施步骤 + +### 3.1. 仓库层 (`DeviceRepository`) + +- **动作**: 在 `internal/infra/repository/device_repository.go` 的 `DeviceRepository` 接口中,增加一个新方法 + `IsDeviceInUse(deviceID uint) (bool, error)`。 +- **实现**: 在 `gormDeviceRepository` 中实现此方法。该方法将通过对 `models.DeviceTask` 模型执行 `Count` + 操作来高效地判断是否存在 `device_id` 匹配的记录。这比查询完整记录性能更好。 + +### 3.2. 应用层 (`DeviceService`) + +- **动作**: + 1. 在 `internal/app/service/device_service.go` 文件顶部定义一个新的错误变量 `ErrDeviceInUse`,例如 + `var ErrDeviceInUse = errors.New("设备正在被一个或多个任务使用,无法删除")`。 + 2. 修改该文件中的 `DeleteDevice` 方法。 +- **实现**: 在 `DeleteDevice` 方法中,在调用 `s.deviceRepo.Delete()` 之前,先调用我们刚刚创建的 + `s.deviceRepo.IsDeviceInUse()` 方法。如果返回 `true`,则立即返回 `ErrDeviceInUse` 错误,中断删除流程。 + +### 3.3. 表现层 (`DeviceController`) + +- **动作**: 修改 `internal/app/controller/device/device_controller.go` 中的 `DeleteDevice` 方法。 +- **实现**: 在错误处理逻辑中,增加一个 `case` 来专门捕获从服务层返回的 `service.ErrDeviceInUse` + 错误。当捕获到此错误时,返回一个带有明确提示信息(如“设备正在被任务使用,无法删除”)和合适 HTTP 状态码(例如 `409 Conflict`)的错误响应。 + +## 4. 方案优势 + +- **职责清晰**: 业务流程的编排和校验逻辑被正确地放置在应用层,符合您项目清晰的分层架构。 +- **高效查询**: 通过 `COUNT` 查询代替 `Find`,避免了不必要的数据加载,性能更佳。 +- **代码内聚**: 与设备相关的数据库操作都统一封装在 `DeviceRepository` 中。 +- **用户友好**: 通过在控制器层处理特定业务错误,可以给前端返回明确、可操作的错误信息。 \ No newline at end of file diff --git a/design/verification-before-device-deletion/index.md b/design/verification-before-device-deletion/index.md index 218d354..1151947 100644 --- a/design/verification-before-device-deletion/index.md +++ b/design/verification-before-device-deletion/index.md @@ -19,5 +19,6 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/50 3. [现有计划管理逻辑迁移](./plan_service_refactor_to_domain.md) 4. [增加设备任务关联表](./device_task_many_to_many_design.md) 5. [增加任务增删改查时对设备任务关联表的维护](./device_task_association_maintenance.md) +6. [删除设备时检查](./check_before_device_deletion.md) 6. [删除设备模板时检查]() 7. [删除区域主控时检查]() \ No newline at end of file diff --git a/internal/app/controller/device/device_controller.go b/internal/app/controller/device/device_controller.go index 26fa53e..8eb1308 100644 --- a/internal/app/controller/device/device_controller.go +++ b/internal/app/controller/device/device_controller.go @@ -154,12 +154,18 @@ func (c *Controller) DeleteDevice(ctx echo.Context) error { deviceID := ctx.Param("id") if err := c.deviceService.DeleteDevice(deviceID); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { + switch { + case errors.Is(err, gorm.ErrRecordNotFound): c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID) return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID) + case errors.Is(err, service.ErrDeviceInUse): + c.logger.Warnf("%s: 尝试删除正在被使用的设备, ID: %s", actionType, deviceID) + // 返回 409 Conflict 状态码,表示请求与服务器当前状态冲突 + return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "设备正在被使用", deviceID) + default: + c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, deviceID) + return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: "+err.Error(), actionType, "服务层删除失败", deviceID) } - c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, deviceID) - return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: "+err.Error(), actionType, "服务层删除失败", deviceID) } c.logger.Infof("%s: 设备删除成功, ID: %s", actionType, deviceID) diff --git a/internal/app/service/device_service.go b/internal/app/service/device_service.go index 3589d1f..ed2b528 100644 --- a/internal/app/service/device_service.go +++ b/internal/app/service/device_service.go @@ -3,6 +3,7 @@ package service import ( "encoding/json" "errors" + "fmt" "strconv" "git.huangwc.com/pig/pig-farm-controller/internal/app/dto" @@ -11,6 +12,9 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" ) +// ErrDeviceInUse 表示设备正在被任务使用,无法删除 +var ErrDeviceInUse = errors.New("设备正在被一个或多个任务使用,无法删除") + // DeviceService 定义了应用层的设备服务接口,用于协调设备相关的业务逻辑。 type DeviceService interface { CreateDevice(req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error) @@ -142,14 +146,27 @@ func (s *deviceService) DeleteDevice(id string) error { if err != nil { return err } + deviceID := uint(idUint) - // Check if device exists before deleting - _, err = s.deviceRepo.FindByID(uint(idUint)) + // 检查设备是否存在 + _, err = s.deviceRepo.FindByID(deviceID) if err != nil { - return err + return err // 如果未找到,会返回 gorm.ErrRecordNotFound } - return s.deviceRepo.Delete(uint(idUint)) + // 在删除前检查设备是否被任务使用 + inUse, err := s.deviceRepo.IsDeviceInUse(deviceID) + if err != nil { + // 如果检查过程中发生数据库错误,则返回错误 + return fmt.Errorf("检查设备使用情况失败: %w", err) + } + if inUse { + // 如果设备正在被使用,则返回特定的业务错误 + return ErrDeviceInUse + } + + // 只有在未被使用时,才执行删除操作 + return s.deviceRepo.Delete(deviceID) } func (s *deviceService) ManualControl(id string, req *dto.ManualControlDeviceRequest) error { diff --git a/internal/infra/repository/device_repository.go b/internal/infra/repository/device_repository.go index bb0d2f3..908e42f 100644 --- a/internal/infra/repository/device_repository.go +++ b/internal/infra/repository/device_repository.go @@ -43,6 +43,9 @@ type DeviceRepository interface { // GetDevicesByIDsTx 在指定事务中根据ID列表获取设备 GetDevicesByIDsTx(tx *gorm.DB, ids []uint) ([]models.Device, error) + + // IsDeviceInUse 检查设备是否被任何任务使用 + IsDeviceInUse(deviceID uint) (bool, error) } // gormDeviceRepository 是 DeviceRepository 的 GORM 实现 @@ -161,3 +164,14 @@ func (r *gormDeviceRepository) FindByAreaControllerAndPhysicalAddress(areaContro } return &device, nil } + +// IsDeviceInUse 检查设备是否被任何任务使用 +func (r *gormDeviceRepository) IsDeviceInUse(deviceID uint) (bool, error) { + var count int64 + // 直接对 device_tasks 关联表进行 COUNT 操作,性能最高 + err := r.db.Model(&models.DeviceTask{}).Where("device_id = ?", deviceID).Count(&count).Error + if err != nil { + return false, fmt.Errorf("查询设备任务关联失败: %w", err) + } + return count > 0, nil +}