From 6f7e462589982b694c3f55f07bbff05c1e60e180 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 1 Nov 2025 22:43:34 +0800 Subject: [PATCH] =?UTF-8?q?bmad=20=E5=88=86=E6=9E=90=E5=B8=88=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bmad/bmm-workflow-status.yaml | 43 +++ bmad/core/config.yaml | 4 +- bmad/deep-dive-deletion-logic.md | 248 ++++++++++++++++++ bmad/index.md | 6 + internal/app/service/device_service.go | 60 ++++- .../infra/repository/device_repository.go | 35 ++- 6 files changed, 390 insertions(+), 6 deletions(-) create mode 100644 bmad/bmm-workflow-status.yaml create mode 100644 bmad/deep-dive-deletion-logic.md diff --git a/bmad/bmm-workflow-status.yaml b/bmad/bmm-workflow-status.yaml new file mode 100644 index 0000000..0c17e21 --- /dev/null +++ b/bmad/bmm-workflow-status.yaml @@ -0,0 +1,43 @@ +# Workflow Status Template +# This tracks progress through phases 1-3 of the BMM methodology +# Phase 4 (Implementation) is tracked separately in sprint-status.yaml + +# generated: 2025年11月1日星期六 +# project: pig-farm-controller +# project_type: software +# project_level: 0 +# field_type: brownfield +# workflow_path: brownfield-level-0.yaml + +# STATUS DEFINITIONS: +# ================== +# Initial Status (before completion): +# - required: Must be completed to progress +# - optional: Can be completed but not required +# - recommended: Strongly suggested but not required +# - conditional: Required only if certain conditions met (e.g., if_has_ui) +# +# Completion Status: +# - {file-path}: File created/found (e.g., "docs/product-brief.md") +# - skipped: Optional/conditional workflow that was skipped + +generated: "2025年11月1日星期六" +project: "pig-farm-controller" +project_type: "software" +project_level: "0" +field_type: "brownfield" +workflow_path: "brownfield-level-0.yaml" + +workflow_status: " + # Prerequisite: Documentation + document-project: bmad/deep-dive-deletion-logic.md + + # Phase 1: Analysis + brainstorm-project: optional + + # Phase 2: Planning + tech-spec: required + + # Phase 4: Implementation + sprint-planning: required +" \ No newline at end of file diff --git a/bmad/core/config.yaml b/bmad/core/config.yaml index d3fdcfc..ea6e9e7 100644 --- a/bmad/core/config.yaml +++ b/bmad/core/config.yaml @@ -4,6 +4,6 @@ # Date: 2025-11-01T09:44:39.652Z user_name: 主人 -communication_language: zh-CN -document_output_language: zh-CN +communication_language: 中文 +document_output_language: 中文 output_folder: '{project-root}/bmad' diff --git a/bmad/deep-dive-deletion-logic.md b/bmad/deep-dive-deletion-logic.md new file mode 100644 index 0000000..923c515 --- /dev/null +++ b/bmad/deep-dive-deletion-logic.md @@ -0,0 +1,248 @@ +# 深度分析:设备、区域主控和设备模板的删除逻辑 + +## 概述 + +本文档对 `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, ¶ms); 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`。 diff --git a/bmad/index.md b/bmad/index.md index 504a980..94d5462 100644 --- a/bmad/index.md +++ b/bmad/index.md @@ -24,6 +24,12 @@ - [API Contracts](./api-contracts-main.md) - [Data Models](./data-models-main.md) +## Deep-Dive Documentation + +Detailed exhaustive analysis of specific areas: + +- [删除逻辑深度分析](./deep-dive-deletion-logic.md) - 对设备、区域主控和设备模板删除逻辑的全面分析 (文件数: 5, 代码行数: N/A) - 生成日期: 2025年11月1日星期六 + ## Existing Documentation - [README.md](./README.md) - Project README diff --git a/internal/app/service/device_service.go b/internal/app/service/device_service.go index 3589d1f..84fc8f3 100644 --- a/internal/app/service/device_service.go +++ b/internal/app/service/device_service.go @@ -38,7 +38,8 @@ type deviceService struct { deviceRepo repository.DeviceRepository areaControllerRepo repository.AreaControllerRepository deviceTemplateRepo repository.DeviceTemplateRepository - deviceDomainSvc device.Service // 依赖领域服务 + planRepo repository.PlanRepository // 新增计划仓库依赖 + deviceDomainSvc device.Service // 依赖领域服务 } // NewDeviceService 创建一个新的 DeviceService 实例。 @@ -46,16 +47,48 @@ func NewDeviceService( deviceRepo repository.DeviceRepository, areaControllerRepo repository.AreaControllerRepository, deviceTemplateRepo repository.DeviceTemplateRepository, + planRepo repository.PlanRepository, deviceDomainSvc device.Service, ) DeviceService { return &deviceService{ deviceRepo: deviceRepo, areaControllerRepo: areaControllerRepo, deviceTemplateRepo: deviceTemplateRepo, + planRepo: planRepo, deviceDomainSvc: deviceDomainSvc, } } +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 { + // 假设任务参数中设备ID的键是 "device_id" + var params map[string]interface{} + if err := json.Unmarshal(task.Parameters, ¶ms); 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 +} + // --- Devices --- func (s *deviceService) CreateDevice(req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error) { @@ -149,6 +182,13 @@ func (s *deviceService) DeleteDevice(id string) error { 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)) } @@ -263,6 +303,15 @@ func (s *deviceService) DeleteAreaController(id string) error { 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)) } @@ -369,5 +418,14 @@ func (s *deviceService) DeleteDeviceTemplate(id string) error { 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)) } diff --git a/internal/infra/repository/device_repository.go b/internal/infra/repository/device_repository.go index 96104cb..824ead5 100644 --- a/internal/infra/repository/device_repository.go +++ b/internal/infra/repository/device_repository.go @@ -1,6 +1,7 @@ package repository import ( + "errors" "fmt" "strconv" @@ -44,12 +45,22 @@ type DeviceRepository interface { // gormDeviceRepository 是 DeviceRepository 的 GORM 实现 type gormDeviceRepository struct { - db *gorm.DB + db *gorm.DB + deviceCommandLogRepo DeviceCommandLogRepository + pendingCollectionRepo PendingCollectionRepository } // NewGormDeviceRepository 创建一个新的 DeviceRepository GORM 实现实例 -func NewGormDeviceRepository(db *gorm.DB) DeviceRepository { - return &gormDeviceRepository{db: db} +func NewGormDeviceRepository( + db *gorm.DB, + deviceCommandLogRepo DeviceCommandLogRepository, + pendingCollectionRepo PendingCollectionRepository, +) DeviceRepository { + return &gormDeviceRepository{ + db: db, + deviceCommandLogRepo: deviceCommandLogRepo, + pendingCollectionRepo: pendingCollectionRepo, + } } // Create 创建一个新的设备记录 @@ -129,6 +140,24 @@ func (r *gormDeviceRepository) Update(device *models.Device) error { // Delete 根据 ID 删除一个设备 // GORM 使用软删除,记录不会从数据库中物理移除,而是设置 DeletedAt 字段。 func (r *gormDeviceRepository) Delete(id uint) error { + // 检查是否有相关的设备命令日志 + logs, err := r.deviceCommandLogRepo.FindByDeviceID(id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("查询设备 %d 的命令日志失败: %w", id, err) + } + if len(logs) > 0 { + return errors.New("设备有相关的命令日志,不能删除") + } + + // 检查是否有相关的待处理采集请求 + pendingCollections, err := r.pendingCollectionRepo.FindByDeviceID(id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("查询设备 %d 的待处理采集请求失败: %w", id, err) + } + if len(pendingCollections) > 0 { + return errors.New("设备有相关的待处理采集请求,不能删除") + } + return r.db.Delete(&models.Device{}, id).Error }