diff --git a/design/verification-before-device-deletion/index.md b/design/verification-before-device-deletion/index.md index 1151947..9d97ded 100644 --- a/design/verification-before-device-deletion/index.md +++ b/design/verification-before-device-deletion/index.md @@ -20,5 +20,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/50 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 +7. [删除设备模板时检查和删除区域主控时检查](./refactor_deletion_check.md) +8. [优化设备服务方法的入参]() diff --git a/design/verification-before-device-deletion/refactor_deletion_check.md b/design/verification-before-device-deletion/refactor_deletion_check.md new file mode 100644 index 0000000..991d197 --- /dev/null +++ b/design/verification-before-device-deletion/refactor_deletion_check.md @@ -0,0 +1,196 @@ +# 重构方案:将删除前关联检查逻辑迁移至 Service 层 + +## 1. 目标 + +将删除**区域主控 (AreaController)** 和**设备模板 (DeviceTemplate)** 时的关联设备检查逻辑,从 Repository(数据访问)层重构至 Service(业务逻辑)层。 + +## 2. 动机 + +当前实现中,关联检查逻辑位于 Repository 层的 `Delete` 方法内。这违反了分层架构的最佳实践。Repository 层应仅负责单纯的数据持久化操作(增删改查),而不应包含业务规则。 + +通过本次重构,我们将实现: +- **职责分离**: Service 层负责编排业务逻辑(如“删除前必须检查关联”),Repository 层回归其数据访问的单一职责。 +- **代码清晰**: 业务流程在 Service 层一目了然,便于理解和维护。 +- **可测试性增强**: 可以独立测试 Service 层的业务规则,而无需依赖数据库的事务或约束。 + +## 3. 详细实施步骤 + +### 第 1 步:在 Service 层定义业务错误 + +在 `internal/app/service/device_service.go` 文件中,导出两个新的错误变量,用于清晰地表达业务约束。 + +```go +// ErrAreaControllerInUse 表示区域主控正在被设备使用,无法删除 +var ErrAreaControllerInUse = errors.New("区域主控正在被一个或多个设备使用,无法删除") + +// ErrDeviceTemplateInUse 表示设备模板正在被设备使用,无法删除 +var ErrDeviceTemplateInUse = errors.New("设备模板正在被一个或多个设备使用,无法删除") +``` + +### 第 2 步:调整 Repository 接口与实现 + +#### 2.1 `device_repository.go` +在 `DeviceRepository` 接口中增加一个方法,用于检查区域主控是否被使用,并在 `gormDeviceRepository` 中实现它。 + +```go +// internal/infra/repository/device_repository.go + +type DeviceRepository interface { + // ... 其他方法 + IsAreaControllerInUse(areaControllerID uint) (bool, error) +} + +func (r *gormDeviceRepository) IsAreaControllerInUse(areaControllerID uint) (bool, error) { + var count int64 + if err := r.db.Model(&models.Device{}).Where("area_controller_id = ?", areaControllerID).Count(&count).Error; err != nil { + return false, fmt.Errorf("检查区域主控使用情况失败: %w", err) + } + return count > 0, nil +} +``` + +#### 2.2 `area_controller_repository.go` +简化 `Delete` 方法,移除所有业务逻辑,使其成为一个纯粹的数据库删除操作。 + +```go +// internal/infra/repository/area_controller_repository.go + +func (r *gormAreaControllerRepository) Delete(id uint) error { + // 移除原有的事务和关联检查 + if err := r.db.Delete(&models.AreaController{}, id).Error; err != nil { + return fmt.Errorf("删除区域主控失败: %w", err) + } + return nil +} +``` + +#### 2.3 `device_template_repository.go` +同样,简化 `Delete` 方法。`IsInUse` 方法保持不变,因为它仍然是一个有用的查询。 + +```go +// internal/infra/repository/device_template_repository.go + +func (r *gormDeviceTemplateRepository) Delete(id uint) error { + // 移除原有的关联检查逻辑 + if err := r.db.Delete(&models.DeviceTemplate{}, id).Error; err != nil { + return fmt.Errorf("删除设备模板失败: %w", err) + } + return nil +} +``` + +### 第 3 步:在 Service 层实现业务逻辑 + +#### 3.1 `device_service.go` - 删除区域主控 +修改 `DeleteAreaController` 方法,加入关联检查的业务逻辑。 + +```go +// internal/app/service/device_service.go + +func (s *deviceService) DeleteAreaController(id string) error { + idUint, err := strconv.ParseUint(id, 10, 64) + if err != nil { + return fmt.Errorf("无效的ID格式: %w", err) + } + acID := uint(idUint) + + // 1. 检查是否存在 + _, err = s.areaControllerRepo.FindByID(acID) + if err != nil { + return err // 如果未找到,gorm会返回 ErrRecordNotFound + } + + // 2. 检查是否被使用(业务逻辑) + inUse, err := s.deviceRepo.IsAreaControllerInUse(acID) + if err != nil { + return err // 返回数据库检查错误 + } + if inUse { + return ErrAreaControllerInUse // 返回业务错误 + } + + // 3. 执行删除 + return s.areaControllerRepo.Delete(acID) +} +``` + +#### 3.2 `device_service.go` - 删除设备模板 +修改 `DeleteDeviceTemplate` 方法,加入关联检查的业务逻辑。 + +```go +// internal/app/service/device_service.go + +func (s *deviceService) DeleteDeviceTemplate(id string) error { + idUint, err := strconv.ParseUint(id, 10, 64) + if err != nil { + return fmt.Errorf("无效的ID格式: %w", err) + } + dtID := uint(idUint) + + // 1. 检查是否存在 + _, err = s.deviceTemplateRepo.FindByID(dtID) + if err != nil { + return err + } + + // 2. 检查是否被使用(业务逻辑) + inUse, err := s.deviceTemplateRepo.IsInUse(dtID) + if err != nil { + return err + } + if inUse { + return ErrDeviceTemplateInUse // 返回业务错误 + } + + // 3. 执行删除 + return s.deviceTemplateRepo.Delete(dtID) +} +``` + +### 第 4 步:在 Controller 层处理新的业务错误 + +#### 4.1 `device_controller.go` - 删除区域主控 +在 `DeleteAreaController` 的错误处理中,增加对 `ErrAreaControllerInUse` 的捕获,并返回 `409 Conflict` 状态码。 + +```go +// internal/app/controller/device/device_controller.go + +func (c *Controller) DeleteAreaController(ctx echo.Context) error { + // ... + if err := c.deviceService.DeleteAreaController(acID); err != nil { + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + // ... + case errors.Is(err, service.ErrAreaControllerInUse): // 新增 + c.logger.Warnf("%s: 尝试删除正在被使用的主控, ID: %s", actionType, acID) + return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "主控正在被使用", acID) + default: + // ... + } + } + // ... +} +``` + +#### 4.2 `device_controller.go` - 删除设备模板 +在 `DeleteDeviceTemplate` 的错误处理中,增加对 `ErrDeviceTemplateInUse` 的捕获,并返回 `409 Conflict` 状态码。 + +```go +// internal/app/controller/device/device_controller.go + +func (c *Controller) DeleteDeviceTemplate(ctx echo.Context) error { + // ... + if err := c.deviceService.DeleteDeviceTemplate(dtID); err != nil { + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + // ... + case errors.Is(err, service.ErrDeviceTemplateInUse): // 新增 + c.logger.Warnf("%s: 尝试删除正在被使用的模板, ID: %s", actionType, dtID) + return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "模板正在被使用", dtID) + default: + // ... + } + } + // ... +} +``` \ 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 8eb1308..9e20228 100644 --- a/internal/app/controller/device/device_controller.go +++ b/internal/app/controller/device/device_controller.go @@ -331,12 +331,17 @@ func (c *Controller) DeleteAreaController(ctx echo.Context) error { acID := ctx.Param("id") if err := c.deviceService.DeleteAreaController(acID); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { + switch { + case errors.Is(err, gorm.ErrRecordNotFound): c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID) return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID) + case errors.Is(err, service.ErrAreaControllerInUse): + c.logger.Warnf("%s: 尝试删除正在被使用的主控, ID: %s", actionType, acID) + return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "主控正在被使用", acID) + default: + c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, acID) + return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: "+err.Error(), actionType, "服务层删除失败", acID) } - c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, acID) - return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: "+err.Error(), actionType, "服务层删除失败", acID) } c.logger.Infof("%s: 区域主控删除成功, ID: %s", actionType, acID) @@ -469,12 +474,17 @@ func (c *Controller) DeleteDeviceTemplate(ctx echo.Context) error { dtID := ctx.Param("id") if err := c.deviceService.DeleteDeviceTemplate(dtID); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { + switch { + case errors.Is(err, gorm.ErrRecordNotFound): c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID) return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID) + case errors.Is(err, service.ErrDeviceTemplateInUse): + c.logger.Warnf("%s: 尝试删除正在被使用的模板, ID: %s", actionType, dtID) + return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "模板正在被使用", dtID) + default: + c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, dtID) + return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: "+err.Error(), actionType, "服务层删除失败", dtID) } - c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, dtID) - return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: "+err.Error(), actionType, "服务层删除失败", dtID) } c.logger.Infof("%s: 设备模板删除成功, ID: %s", actionType, dtID) diff --git a/internal/app/service/device_service.go b/internal/app/service/device_service.go index ed2b528..8530d98 100644 --- a/internal/app/service/device_service.go +++ b/internal/app/service/device_service.go @@ -12,8 +12,16 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" ) -// ErrDeviceInUse 表示设备正在被任务使用,无法删除 -var ErrDeviceInUse = errors.New("设备正在被一个或多个任务使用,无法删除") +var ( + // ErrDeviceInUse 表示设备正在被任务使用,无法删除 + ErrDeviceInUse = errors.New("设备正在被一个或多个任务使用,无法删除") + + // ErrAreaControllerInUse 表示区域主控正在被设备使用,无法删除 + ErrAreaControllerInUse = errors.New("区域主控正在被一个或多个设备使用,无法删除") + + // ErrDeviceTemplateInUse 表示设备模板正在被设备使用,无法删除 + ErrDeviceTemplateInUse = errors.New("设备模板正在被一个或多个设备使用,无法删除") +) // DeviceService 定义了应用层的设备服务接口,用于协调设备相关的业务逻辑。 type DeviceService interface { @@ -272,15 +280,27 @@ func (s *deviceService) UpdateAreaController(id string, req *dto.UpdateAreaContr func (s *deviceService) DeleteAreaController(id string) error { idUint, err := strconv.ParseUint(id, 10, 64) if err != nil { - return err + return fmt.Errorf("无效的ID格式: %w", err) } + acID := uint(idUint) - _, err = s.areaControllerRepo.FindByID(uint(idUint)) + // 1. 检查是否存在 + _, err = s.areaControllerRepo.FindByID(acID) if err != nil { - return err + return err // 如果未找到,gorm会返回 ErrRecordNotFound } - return s.areaControllerRepo.Delete(uint(idUint)) + // 2. 检查是否被使用(业务逻辑) + inUse, err := s.deviceRepo.IsAreaControllerInUse(acID) + if err != nil { + return err // 返回数据库检查错误 + } + if inUse { + return ErrAreaControllerInUse // 返回业务错误 + } + + // 3. 执行删除 + return s.areaControllerRepo.Delete(acID) } // --- Device Templates --- @@ -378,13 +398,25 @@ func (s *deviceService) UpdateDeviceTemplate(id string, req *dto.UpdateDeviceTem func (s *deviceService) DeleteDeviceTemplate(id string) error { idUint, err := strconv.ParseUint(id, 10, 64) if err != nil { - return err + return fmt.Errorf("无效的ID格式: %w", err) } + dtID := uint(idUint) - _, err = s.deviceTemplateRepo.FindByID(uint(idUint)) + // 1. 检查是否存在 + _, err = s.deviceTemplateRepo.FindByID(dtID) if err != nil { return err } - return s.deviceTemplateRepo.Delete(uint(idUint)) + // 2. 检查是否被使用(业务逻辑) + inUse, err := s.deviceTemplateRepo.IsInUse(dtID) + if err != nil { + return err + } + if inUse { + return ErrDeviceTemplateInUse // 返回业务错误 + } + + // 3. 执行删除 + return s.deviceTemplateRepo.Delete(dtID) } diff --git a/internal/infra/repository/area_controller_repository.go b/internal/infra/repository/area_controller_repository.go index 7730ca5..45b5737 100644 --- a/internal/infra/repository/area_controller_repository.go +++ b/internal/infra/repository/area_controller_repository.go @@ -47,26 +47,11 @@ func (r *gormAreaControllerRepository) Update(ac *models.AreaController) error { } // Delete 删除一个 AreaController 记录。 -// 在删除前会检查是否有设备关联到该主控,如果有,则不允许删除。 func (r *gormAreaControllerRepository) Delete(id uint) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // 检查是否有设备关联到这个区域主控 - var count int64 - if err := tx.Model(&models.Device{}).Where("area_controller_id = ?", id).Count(&count).Error; err != nil { - return fmt.Errorf("检查关联设备失败: %w", err) - } - - if count > 0 { - return fmt.Errorf("无法删除区域主控,因为仍有 %d 个设备关联到它", count) - } - - // 如果没有关联设备,则执行删除操作 - if err := tx.Delete(&models.AreaController{}, id).Error; err != nil { - return fmt.Errorf("删除区域主控失败: %w", err) - } - - return nil - }) + if err := r.db.Delete(&models.AreaController{}, id).Error; err != nil { + return fmt.Errorf("删除区域主控失败: %w", err) + } + return nil } // FindByID 通过 ID 查找一个 AreaController。 diff --git a/internal/infra/repository/device_repository.go b/internal/infra/repository/device_repository.go index 908e42f..8b2d167 100644 --- a/internal/infra/repository/device_repository.go +++ b/internal/infra/repository/device_repository.go @@ -46,6 +46,9 @@ type DeviceRepository interface { // IsDeviceInUse 检查设备是否被任何任务使用 IsDeviceInUse(deviceID uint) (bool, error) + + // IsAreaControllerInUse 检查区域主控是否被任何设备使用 + IsAreaControllerInUse(areaControllerID uint) (bool, error) } // gormDeviceRepository 是 DeviceRepository 的 GORM 实现 @@ -175,3 +178,12 @@ func (r *gormDeviceRepository) IsDeviceInUse(deviceID uint) (bool, error) { } return count > 0, nil } + +// IsAreaControllerInUse 检查区域主控是否被任何设备使用 +func (r *gormDeviceRepository) IsAreaControllerInUse(areaControllerID uint) (bool, error) { + var count int64 + if err := r.db.Model(&models.Device{}).Where("area_controller_id = ?", areaControllerID).Count(&count).Error; err != nil { + return false, fmt.Errorf("检查区域主控使用情况失败: %w", err) + } + return count > 0, nil +} diff --git a/internal/infra/repository/device_template_repository.go b/internal/infra/repository/device_template_repository.go index 442ca3b..5872f50 100644 --- a/internal/infra/repository/device_template_repository.go +++ b/internal/infra/repository/device_template_repository.go @@ -83,12 +83,8 @@ func (r *gormDeviceTemplateRepository) IsInUse(id uint) (bool, error) { // Delete 软删除数据库中的设备模板 func (r *gormDeviceTemplateRepository) Delete(id uint) error { - inUse, err := r.IsInUse(id) - if err != nil { - return err + if err := r.db.Delete(&models.DeviceTemplate{}, id).Error; err != nil { + return fmt.Errorf("删除设备模板失败: %w", err) } - if inUse { - return errors.New("设备模板正在被设备使用,无法删除") - } - return r.db.Delete(&models.DeviceTemplate{}, id).Error + return nil }