# 深度分析：设备、区域主控和设备模板的删除逻辑

## 概述

本文档对 `pig-farm-controller` 项目中设备、区域主控和设备模板的删除逻辑进行了深度分析。重点关注了 API 入口点、控制器层、服务层以及存储库层的实现，并详细描述了为解决 Issue #50（删除时需检测资源是否被使用）而进行的修改。

## 1. API 入口点 (router.go)

API 路由在 `internal/app/api/router.go` 中定义。以下是与删除操作相关的路由：

*   **删除设备:**
    ```go
    deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice)
    ```
    路径: `/api/v1/devices/{id}`
    处理函数: `a.deviceController.DeleteDevice`

*   **删除区域主控:**
    ```go
    areaControllerGroup.DELETE("/:id", a.deviceController.DeleteAreaController)
    ```
    路径: `/api/v1/area-controllers/{id}`
    处理函数: `a.deviceController.DeleteAreaController`

*   **删除设备模板:**
    ```go
    deviceTemplateGroup.DELETE("/:id", a.deviceController.DeleteDeviceTemplate)
    ```
    路径: `/api/v1/device-templates/{id}`
    处理函数: `a.deviceController.DeleteDeviceTemplate`

所有这些删除操作都通过 HTTP `DELETE` 方法，并由 `deviceController` 中的相应函数处理。

## 2. 控制器层 (device_controller.go)

控制器层位于 `internal/app/controller/device/device_controller.go`。`Controller` 结构体封装了设备、区域主控和设备模板相关的业务逻辑。

### `DeleteDevice(ctx echo.Context) error`

*   从请求上下文中获取设备 ID。
*   调用 `c.deviceService.DeleteDevice(deviceID)` 执行删除操作。
*   处理服务层返回的错误，包括 `gorm.ErrRecordNotFound` 和其他内部错误。
*   成功时返回成功响应。

### `DeleteAreaController(ctx echo.Context) error`

*   从请求上下文中获取区域主控 ID。
*   调用 `c.deviceService.DeleteAreaController(acID)` 执行删除操作。
*   处理服务层返回的错误。
*   成功时返回成功响应。

### `DeleteDeviceTemplate(ctx echo.Context) error`

*   从请求上下文中获取设备模板 ID。
*   调用 `c.deviceService.DeleteDeviceTemplate(dtID)` 执行删除操作。
*   处理服务层返回的错误。
*   成功时返回成功响应。

## 3. 服务层 (device_service.go)

服务层位于 `internal/app/service/device_service.go`。`deviceService` 结构体实现了 `DeviceService` 接口，并依赖于 `deviceRepo`、`areaControllerRepo`、`deviceTemplateRepo` 和 `planRepo`。

### `DeleteDevice(id string) error`

**修改前:**
```go
func (s *deviceService) DeleteDevice(id string) error {
    idUint, err := strconv.ParseUint(id, 10, 64)
    if err != nil {
        return err
    }
    _, err = s.deviceRepo.FindByID(uint(idUint))
    if err != nil {
        return err
    }
    return s.deviceRepo.Delete(uint(idUint))
}
```

**修改后 (新增“使用中”检查):**
```go
func (s *deviceService) DeleteDevice(id string) error {
    idUint, err := strconv.ParseUint(id, 10, 64)
    if err != nil {
        return err
    }

    // Check if device exists before deleting
    _, err = s.deviceRepo.FindByID(uint(idUint))
    if err != nil {
        return err
    }

    // 检查设备是否被活动计划使用
    if inUse, err := s.isDeviceInUseByActivePlan(uint(idUint)); err != nil {
        return fmt.Errorf("检查设备是否被计划使用失败: %w", err)
    } else if inUse {
        return errors.New("设备正在被活动计划使用，无法删除")
    }

    return s.deviceRepo.Delete(uint(idUint))
}
```
**新增 `isDeviceInUseByActivePlan` 辅助方法:**
```go
func (s *deviceService) isDeviceInUseByActivePlan(deviceID uint) (bool, error) {
    plans, err := s.planRepo.FindPlansWithPendingTasks()
    if err != nil {
        return false, fmt.Errorf("查询活动计划失败: %w", err)
    }

    for _, plan := range plans {
        tasks, err := s.planRepo.FlattenPlanTasks(plan.ID)
        if err != nil {
            return false, fmt.Errorf("展开计划 %d 的任务失败: %w", plan.ID, err)
        }

        for _, task := range tasks {
            var params map[string]interface{}
            if err := json.Unmarshal(task.Parameters, &params); err != nil {
                continue
            }

            if paramDeviceID, ok := params["device_id"]; ok {
                if floatDeviceID, ok := paramDeviceID.(float64); ok && uint(floatDeviceID) == deviceID {
                    return true, nil
                }
            }
        }
    }
    return false, nil
}
```
此方法现在会在删除设备前，检查该设备是否被任何活动计划中的任务所引用。

### `DeleteAreaController(id string) error`

**修改前:**
```go
func (s *deviceService) DeleteAreaController(id string) error {
    idUint, err := strconv.ParseUint(id, 10, 64)
    if err != nil {
        return err
    }
    _, err = s.areaControllerRepo.FindByID(uint(idUint))
    if err != nil {
        return err
    }
    return s.areaControllerRepo.Delete(uint(idUint))
}
```

**修改后 (新增“使用中”检查):**
```go
func (s *deviceService) DeleteAreaController(id string) error {
    idUint, err := strconv.ParseUint(id, 10, 64)
    if err != nil {
        return err
    }

    _, err = s.areaControllerRepo.FindByID(uint(idUint))
    if err != nil {
        return err
    }

    // 检查是否有设备正在使用此区域主控
    devices, err := s.deviceRepo.ListByAreaControllerID(uint(idUint))
    if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
        return err
    }
    if len(devices) > 0 {
        return errors.New("区域主控正在被设备使用，无法删除")
    }

    return s.areaControllerRepo.Delete(uint(idUint))
}
```
此方法现在会在删除区域主控前，检查是否有设备正在使用该区域主控。

### `DeleteDeviceTemplate(id string) error`

**修改前:**
```go
func (s *deviceService) DeleteDeviceTemplate(id string) error {
    idUint, err := strconv.ParseUint(id, 10, 64)
    if err != nil {
        return err
    }
    _, err = s.deviceTemplateRepo.FindByID(uint(idUint))
    if err != nil {
        return err
    }
    return s.deviceTemplateRepo.Delete(uint(idUint))
}
```

**修改后 (新增“使用中”检查):**
```go
func (s *deviceService) DeleteDeviceTemplate(id string) error {
    idUint, err := strconv.ParseUint(id, 10, 64)
    if err != nil {
        return err
    }

    _, err = s.deviceTemplateRepo.FindByID(uint(idUint))
    if err != nil {
        return err
    }

    // 检查是否有设备正在使用此模板
    devices, err := s.deviceRepo.FindByDeviceTemplateID(uint(idUint))
    if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
        return err
    }
    if len(devices) > 0 {
        return errors.New("设备模板正在被设备使用，无法删除")
    }

    return s.deviceTemplateRepo.Delete(uint(idUint))
}
```
此方法现在会在删除设备模板前，检查是否有设备正在使用该设备模板。

## 4. 存储库层

### `device_repository.go`

*   **`FindByDeviceTemplateID(deviceTemplateID uint) ([]*models.Device, error)`**: 用于查找所有使用特定设备模板的设备。
*   **`ListByAreaControllerID(areaControllerID uint) ([]*models.Device, error)`**: 用于查找所有与特定区域主控关联的设备。
*   **`Delete(id uint) error`**: 此方法已包含对 `deviceCommandLogRepo` 和 `pendingCollectionRepo` 的检查，确保设备在删除前没有相关的命令日志或待处理采集请求。

### `plan_repository.go`

*   **`FindPlansWithPendingTasks() ([]*models.Plan, error)`**: 用于查找所有具有待处理任务的活动计划。
*   **`FlattenPlanTasks(planID uint) ([]models.Task, error)`**: 用于递归展开计划，获取其包含的所有任务。

### `internal/infra/models/plan.go`

*   **`Task` 结构体**: `Parameters datatypes.JSON` 字段用于存储任务特定参数，其中可能包含 `device_id`。

## 5. 总结与改进

通过在服务层添加“使用中”检查，我们确保了在删除设备、区域主控和设备模板时，如果它们正在被其他资源（如设备或活动计划）使用，则会阻止删除操作并返回相应的错误信息。这解决了 Issue #50 中提出的问题，提高了系统的健壮性和数据一致性。

未来的改进可以包括：
*   为“使用中”检查提供更详细的错误信息，例如列出正在使用该资源的具体设备或计划。
*   考虑在删除操作中引入级联删除或强制删除选项（需谨慎）。
*   为 `Task` 的 `Parameters` 定义更严格的结构体，以便更安全地解析和访问其中的 `device_id`。
