Files
pig-farm-controller/design/verification-before-device-deletion/refactor_deletion_check.md

196 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 重构方案:将删除前关联检查逻辑迁移至 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:
// ...
}
}
// ...
}
```