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