增加任务增删改查时对设备任务关联表的维护
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
# 方案:维护设备与任务的关联关系
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在对计划(Plan)及其包含的任务(Task)进行创建、更新、删除(CRUD)操作时,同步维护 `device_tasks` 这张多对多关联表。
|
||||
|
||||
这是实现“删除设备前检查其是否被任务使用”这一需求的基础。
|
||||
|
||||
## 2. 核心挑战
|
||||
|
||||
1. **参数结构异构性**:不同类型的任务(`TaskType`),其设备 ID 存储在 `Parameters` (JSON) 字段中的 `key` 和数据结构(单个 ID
|
||||
或 ID 数组)各不相同。
|
||||
2. **分层架构原则**:解析 `Parameters` 以提取设备 ID 的逻辑属于 **业务规则**,需要找到一个合适的位置来封装它,以维持各层职责的清晰。
|
||||
|
||||
## 3. 方案设计
|
||||
|
||||
本方案旨在最大化地复用现有领域模型和逻辑,通过扩展 `TaskFactory` 来实现设备ID的解析,从而保持了各领域模块的高内聚和低耦合。
|
||||
|
||||
### 3.1. 核心思路:复用领域对象与工厂
|
||||
|
||||
我们不移动任何结构体,也不在 `plan` 包中引入任何具体任务的实现细节。取而代之,我们利用现有的 `TaskFactory`
|
||||
和各个任务领域对象自身的能力来解析参数。
|
||||
|
||||
每个具体的任务领域对象(如 `ReleaseFeedWeightTask`)最了解如何解析自己的 `Parameters`。因此,我们将解析设备ID的责任完全交还给它们。
|
||||
|
||||
### 3.2. 扩展 `TaskFactory`
|
||||
|
||||
- **动作**:在 `plan.TaskFactory` 接口中增加一个新方法 `CreateTaskFromModel(*models.Task) (TaskDeviceIDResolver, error)`。
|
||||
- **目的**:此方法允许我们在非任务执行的场景下(例如,在增删改查计划时),仅根据数据库模型 `models.Task` 来创建一个临时的、轻量级的任务领域对象。
|
||||
- **实现**:在 `internal/domain/task/task.go` 的 `taskFactory` 中实现此方法。它会根据传入的 `taskModel.Type`,`switch-case`
|
||||
来调用相应的构造函数(如 `NewReleaseFeedWeightTask`)创建实例。
|
||||
- **实现**:
|
||||
- **优势**:
|
||||
- **高内聚,低耦合**:`plan` 包保持通用,无需了解任何具体任务的参数细节。参数定义和解析逻辑都保留在各自的 `task` 包内。
|
||||
- **逻辑复用**:完美复用了您已在 `ReleaseFeedWeightTask` 中实现的 `ResolveDeviceIDs` 方法,避免了重复代码。
|
||||
|
||||
### 3.3. 调整领域服务层 (`PlanService`)
|
||||
|
||||
`PlanService` 将作为此业务用例的核心编排者。借助 `UnitOfWork` 模式,它可以在单个事务中协调多个仓库,完成数据准备和持久化。
|
||||
|
||||
- **职责**:在创建或更新计划的业务流程中,负责解析任务参数、准备设备关联数据,并调用仓库层完成持久化。
|
||||
- **实现**:
|
||||
- 向 `planServiceImpl` 注入 `repository.UnitOfWork` 和 `plan.TaskFactory`。
|
||||
- 在 `CreatePlan` 和 `UpdatePlan` 方法中,使用 `unitOfWork.ExecuteInTransaction` 来包裹整个操作。
|
||||
- 在事务闭包内,遍历计划中的所有任务 (`models.Task`):
|
||||
1. 调用 `taskFactory.CreateTaskFromModel(taskModel)` 创建一个临时的任务领域对象。
|
||||
2. 调用该领域对象的 `ResolveDeviceIDs()` 方法获取设备ID列表。
|
||||
3. 使用事务性的 `DeviceRepository` 查询出设备实体。
|
||||
4. 将查询到的设备实体列表填充到 `taskModel.Devices` 字段中。
|
||||
- 最后,将填充好关联数据的 `plan` 对象传递给事务性的 `PlanRepository` 进行创建或更新。
|
||||
- **优势**:
|
||||
- **职责清晰**:`PlanService` 完整地拥有了“创建/更新计划”的业务逻辑,而仓库层则回归到纯粹的数据访问职责。
|
||||
- **数据一致性**:`UnitOfWork` 确保了从准备数据(查询设备)到最终持久化(创建计划和关联)的所有数据库操作都在一个原子事务中完成。
|
||||
|
||||
### 3.4. 调整仓库层 (`PlanRepository`)
|
||||
|
||||
仓库层被简化,回归其作为数据持久化网关的纯粹角色。
|
||||
|
||||
- **职责**:负责 `Plan` 及其直接子对象(`Task`, `SubPlan`)的 CRUD 操作。
|
||||
- **实现**:
|
||||
- `CreatePlan` 和 `UpdatePlanMetadataAndStructure` 方法将被简化。它们不再需要任何特殊的关联处理逻辑(如 `Association().Replace()`)。
|
||||
- 只需接收一个由 `PlanService` 准备好的、`task.Devices` 字段已被填充的 `plan` 对象。
|
||||
- 在 `CreatePlan` 中,调用 `tx.Create(plan)` 时,GORM 会自动级联创建 `Plan`、`Task` 以及 `device_tasks` 中的关联记录。
|
||||
- 在 `UpdatePlanMetadataAndStructure` 的 `reconcileTasks` 逻辑中,对于新创建的任务,GORM 的 `tx.Create(task)` 同样会自动处理其设备关联。
|
||||
|
||||
### 3.5. 整体流程
|
||||
|
||||
以 **创建计划** 为例:
|
||||
|
||||
1. `PlanController` 调用 `PlanService.CreatePlan(plan)`。
|
||||
2. `PlanService` 调用 `unitOfWork.ExecuteInTransaction` 启动一个数据库事务。
|
||||
3. 在事务闭包内,`PlanService` 遍历 `plan` 对象中的所有 `task`。
|
||||
4. 对于每一个 `task` 模型,调用 `taskFactory.CreateTaskFromModel(task)` 创建一个临时的领域对象。
|
||||
5. 调用该领域对象的 `ResolveDeviceIDs()` 方法,获取其使用的设备 ID 列表。
|
||||
6. 如果返回了设备 ID 列表,则使用事务性的 `DeviceRepository` 查询出 `[]models.Device` 实体。
|
||||
7. 所有 `task` 的关联数据准备好后,调用事务性的 `PlanRepository.CreatePlan(plan)`。GORM 在创建 `plan` 和 `task` 的同时,会自动创建
|
||||
`device_tasks` 表中的关联记录。
|
||||
8. `UnitOfWork` 提交事务。
|
||||
|
||||
**更新计划** 的流程与创建类似,在 `UpdatePlanMetadataAndStructure` 方法中,由于会先删除旧任务再创建新任务,因此在创建新任务后执行相同的设备关联步骤。
|
||||
|
||||
**删除计划** 时,由于 `Task` 模型上配置了 `OnDelete:CASCADE`,GORM 会自动删除关联的 `Task` 记录。同时,GORM 的多对多删除逻辑会自动清理
|
||||
`device_tasks` 表中与被删除任务相关的记录。因此 `DeletePlan` 方法无需修改。
|
||||
|
||||
## 4. 实施步骤
|
||||
|
||||
1. **扩展 `TaskFactory` 接口**
|
||||
- 在 `internal/domain/plan/task.go` 文件中,为 `TaskFactory` 接口添加
|
||||
`CreateTaskFromModel(*models.Task) (TaskDeviceIDResolver, error)` 方法。
|
||||
|
||||
2. **实现 `TaskFactory` 新方法**
|
||||
- 在 `internal/domain/task/task.go` 文件中,为 `taskFactory` 结构体实现 `CreateTaskFromModel` 方法。
|
||||
|
||||
3. **修改 `PlanService`**
|
||||
- 在 `internal/domain/plan/plan_service.go` 中:
|
||||
- 修改 `planServiceImpl` 结构体,增加 `unitOfWork repository.UnitOfWork` 和 `taskFactory TaskFactory` 字段。
|
||||
- 修改 `NewPlanService` 构造函数,接收并注入这些新依赖。
|
||||
- 重构 `CreatePlan` 和 `UpdatePlan` 方法,使用 `UnitOfWork` 包裹事务,并在其中实现数据准备和关联逻辑。
|
||||
|
||||
4. **修改 `PlanRepository`**
|
||||
- 在 `internal/infra/repository/plan_repository.go` 中:
|
||||
- **简化 `CreatePlan` 和 `UpdatePlanMetadataAndStructure` 方法**。移除所有手动处理设备关联的代码(例如,如果之前有 `Association("Devices").Replace()` 等调用,则应删除)。
|
||||
- 确保这两个方法的核心逻辑就是调用 GORM 的 `Create` 或 `Updates`,信任 GORM 会根据传入模型中已填充的 `Devices` 字段来自动维护多对多关联。
|
||||
|
||||
5. **修改依赖注入**
|
||||
- 在 `internal/core/component_initializers.go` (或类似的依赖注入入口文件) 中:
|
||||
- 将 `unitOfWork` 和 `taskFactory` 实例传递给 `plan.NewPlanService` 的构造函数。
|
||||
|
||||
## 5. 结论
|
||||
|
||||
此方案通过复用现有的领域对象和工厂模式,优雅地解决了设备关联维护的问题。它保持了清晰的架构分层和模块职责,在实现功能的同时,为项目未来的扩展和维护奠定了坚实、可扩展的基础。
|
||||
@@ -18,6 +18,6 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/50
|
||||
2. [让任务可以提供自身使用设备](./add_get_device_id_configs_to_task.md)
|
||||
3. [现有计划管理逻辑迁移](./plan_service_refactor_to_domain.md)
|
||||
4. [增加设备任务关联表](./device_task_many_to_many_design.md)
|
||||
5. [增加设备增删改查时对设备任务关联表的维护]()
|
||||
5. [增加任务增删改查时对设备任务关联表的维护](./device_task_association_maintenance.md)
|
||||
6. [删除设备模板时检查]()
|
||||
7. [删除区域主控时检查]()
|
||||
@@ -170,7 +170,14 @@ func initDomainServices(cfg *config.Config, infra *Infrastructure, logger *logs.
|
||||
)
|
||||
|
||||
// 计划管理器
|
||||
planService := plan.NewPlanService(planExecutionManager, analysisPlanTaskManager, infra.repos.planRepo, logger)
|
||||
planService := plan.NewPlanService(
|
||||
planExecutionManager,
|
||||
analysisPlanTaskManager,
|
||||
infra.repos.planRepo,
|
||||
infra.repos.deviceRepo,
|
||||
infra.repos.unitOfWork,
|
||||
taskFactory,
|
||||
logger)
|
||||
|
||||
return &DomainServices{
|
||||
pigPenTransferManager: pigPenTransferManager,
|
||||
|
||||
1
internal/domain/plan/device_id_extractor.go
Normal file
1
internal/domain/plan/device_id_extractor.go
Normal file
@@ -0,0 +1 @@
|
||||
package plan
|
||||
@@ -2,6 +2,7 @@ package plan
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
@@ -55,25 +56,31 @@ type Service interface {
|
||||
type planServiceImpl struct {
|
||||
executionManager ExecutionManager
|
||||
taskManager AnalysisPlanTaskManager
|
||||
planRepo repository.PlanRepository // 新增
|
||||
// deviceRepo repository.DeviceRepository // 如果需要,新增
|
||||
logger *logs.Logger
|
||||
planRepo repository.PlanRepository
|
||||
deviceRepo repository.DeviceRepository
|
||||
unitOfWork repository.UnitOfWork
|
||||
taskFactory TaskFactory
|
||||
logger *logs.Logger
|
||||
}
|
||||
|
||||
// NewPlanService 创建一个新的 Service 实例。
|
||||
func NewPlanService(
|
||||
executionManager ExecutionManager,
|
||||
taskManager AnalysisPlanTaskManager,
|
||||
planRepo repository.PlanRepository, // 新增
|
||||
// deviceRepo repository.DeviceRepository, // 如果需要,新增
|
||||
planRepo repository.PlanRepository,
|
||||
deviceRepo repository.DeviceRepository,
|
||||
unitOfWork repository.UnitOfWork,
|
||||
taskFactory TaskFactory,
|
||||
logger *logs.Logger,
|
||||
) Service {
|
||||
return &planServiceImpl{
|
||||
executionManager: executionManager,
|
||||
taskManager: taskManager,
|
||||
planRepo: planRepo, // 注入
|
||||
// deviceRepo: deviceRepo, // 注入
|
||||
logger: logger,
|
||||
planRepo: planRepo,
|
||||
deviceRepo: deviceRepo,
|
||||
unitOfWork: unitOfWork,
|
||||
taskFactory: taskFactory,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,13 +124,40 @@ func (s *planServiceImpl) CreatePlan(planToCreate *models.Plan) (*models.Plan, e
|
||||
}
|
||||
planToCreate.ReorderSteps()
|
||||
|
||||
// 3. 调用仓库方法创建计划
|
||||
if err := s.planRepo.CreatePlan(planToCreate); err != nil {
|
||||
// 3. 在调用仓库前,准备好所有数据,包括设备关联
|
||||
for i := range planToCreate.Tasks {
|
||||
taskModel := &planToCreate.Tasks[i]
|
||||
// 使用工厂创建临时领域对象
|
||||
taskResolver, err := s.taskFactory.CreateTaskFromModel(taskModel)
|
||||
if err != nil {
|
||||
// 如果一个任务类型不支持,我们可以选择跳过或报错
|
||||
s.logger.Warnf("跳过为任务类型 '%s' 解析设备ID: %v", taskModel.Type, err)
|
||||
continue
|
||||
}
|
||||
|
||||
deviceIDs, err := taskResolver.ResolveDeviceIDs()
|
||||
if err != nil {
|
||||
// 在事务外解析失败,直接返回错误
|
||||
return nil, fmt.Errorf("为任务 '%s' 提取设备ID失败: %w", taskModel.Name, err)
|
||||
}
|
||||
if len(deviceIDs) > 0 {
|
||||
// 优化:无需查询完整的设备对象,只需构建包含ID的结构体即可建立关联
|
||||
devices := make([]models.Device, len(deviceIDs))
|
||||
for i, id := range deviceIDs {
|
||||
devices[i] = models.Device{Model: gorm.Model{ID: id}}
|
||||
}
|
||||
taskModel.Devices = devices
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 调用仓库方法创建计划,该方法内部会处理事务
|
||||
err := s.planRepo.CreatePlan(planToCreate)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列
|
||||
// 5. 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列
|
||||
if err := s.taskManager.EnsureAnalysisTaskDefinition(planToCreate.ID); err != nil {
|
||||
// 这是一个非阻塞性错误,我们只记录日志,因为主流程(创建计划)已经成功
|
||||
s.logger.Errorf("为新创建的计划 %d 确保触发器任务定义失败: %v", planToCreate.ID, err)
|
||||
@@ -203,7 +237,32 @@ func (s *planServiceImpl) UpdatePlan(planToUpdate *models.Plan) (*models.Plan, e
|
||||
planToUpdate.ExecuteCount = 0
|
||||
s.logger.Infof("计划 #%d 被更新,执行计数器已重置为 0。", planToUpdate.ID)
|
||||
|
||||
if err := s.planRepo.UpdatePlanMetadataAndStructure(planToUpdate); err != nil {
|
||||
// 在调用仓库前,准备好所有数据,包括设备关联
|
||||
for i := range planToUpdate.Tasks {
|
||||
taskModel := &planToUpdate.Tasks[i]
|
||||
taskResolver, err := s.taskFactory.CreateTaskFromModel(taskModel)
|
||||
if err != nil {
|
||||
s.logger.Warnf("跳过为任务类型 '%s' 解析设备ID: %v", taskModel.Type, err)
|
||||
continue
|
||||
}
|
||||
|
||||
deviceIDs, err := taskResolver.ResolveDeviceIDs()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("为任务 '%s' 提取设备ID失败: %w", taskModel.Name, err)
|
||||
}
|
||||
if len(deviceIDs) > 0 {
|
||||
// 优化:无需查询完整的设备对象,只需构建包含ID的结构体即可建立关联
|
||||
devices := make([]models.Device, len(deviceIDs))
|
||||
for i, id := range deviceIDs {
|
||||
devices[i] = models.Device{Model: gorm.Model{ID: id}}
|
||||
}
|
||||
taskModel.Devices = devices
|
||||
}
|
||||
}
|
||||
|
||||
// 调用仓库方法更新计划,该方法内部会处理事务
|
||||
err = s.planRepo.UpdatePlanMetadataAndStructure(planToUpdate)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -29,4 +29,6 @@ type TaskDeviceIDResolver interface {
|
||||
type TaskFactory interface {
|
||||
// Production 根据指定的任务执行日志创建一个任务实例。
|
||||
Production(claimedLog *models.TaskExecutionLog) Task
|
||||
// CreateTaskFromModel 仅根据任务模型创建一个任务实例,用于非执行场景(如参数解析)。
|
||||
CreateTaskFromModel(taskModel *models.Task) (TaskDeviceIDResolver, error)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
@@ -43,3 +45,27 @@ func (t *taskFactory) Production(claimedLog *models.TaskExecutionLog) plan.Task
|
||||
panic("不支持的任务类型") // 显式panic防编译器报错
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTaskFromModel 实现了 TaskFactory 接口,用于从模型创建任务实例。
|
||||
func (t *taskFactory) CreateTaskFromModel(taskModel *models.Task) (plan.TaskDeviceIDResolver, error) {
|
||||
// 这个方法不关心 claimedLog 的其他字段,所以可以构造一个临时的
|
||||
// 它只用于访问那些不依赖于执行日志的方法,比如 ResolveDeviceIDs
|
||||
tempLog := &models.TaskExecutionLog{Task: *taskModel}
|
||||
|
||||
switch taskModel.Type {
|
||||
case models.TaskTypeWaiting:
|
||||
return NewDelayTask(t.logger, tempLog), nil
|
||||
case models.TaskTypeReleaseFeedWeight:
|
||||
return NewReleaseFeedWeightTask(
|
||||
tempLog,
|
||||
t.sensorDataRepo,
|
||||
t.deviceRepo,
|
||||
t.deviceService,
|
||||
t.logger,
|
||||
), nil
|
||||
case models.TaskTypeFullCollection:
|
||||
return NewFullCollectionTask(tempLog, t.deviceRepo, t.deviceService, t.logger), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持为类型 '%s' 的任务创建模型实例", taskModel.Type)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ func GetAllModels() []interface{} {
|
||||
&DeviceTemplate{},
|
||||
&SensorData{},
|
||||
&DeviceCommandLog{},
|
||||
&DeviceTask{},
|
||||
|
||||
// Plan & Task Models
|
||||
&Plan{},
|
||||
|
||||
@@ -17,7 +17,7 @@ type DeviceRepository interface {
|
||||
// FindByID 根据主键 ID 查找设备
|
||||
FindByID(id uint) (*models.Device, error)
|
||||
|
||||
// FindByIDString 根据字符串形式的主键 ID 查找设备,方便控制器调用
|
||||
// FindByIDString 根据字符串形式的主键 ID 查找设备
|
||||
FindByIDString(id string) (*models.Device, error)
|
||||
|
||||
// ListAll 获取所有设备的列表
|
||||
@@ -26,7 +26,7 @@ type DeviceRepository interface {
|
||||
// ListAllSensors 获取所有传感器类型的设备列表
|
||||
ListAllSensors() ([]*models.Device, error)
|
||||
|
||||
// ListByAreaControllerID 根据区域主控 ID 列出所有子设备。
|
||||
// ListByAreaControllerID 根据区域主控 ID 列出所有子设备
|
||||
ListByAreaControllerID(areaControllerID uint) ([]*models.Device, error)
|
||||
|
||||
// FindByDeviceTemplateID 根据设备模板ID查找所有使用该模板的设备
|
||||
@@ -40,6 +40,9 @@ type DeviceRepository interface {
|
||||
|
||||
// FindByAreaControllerAndPhysicalAddress 根据区域主控ID和物理地址(总线号、总线地址)查找设备
|
||||
FindByAreaControllerAndPhysicalAddress(areaControllerID uint, busNumber int, busAddress int) (*models.Device, error)
|
||||
|
||||
// GetDevicesByIDsTx 在指定事务中根据ID列表获取设备
|
||||
GetDevicesByIDsTx(tx *gorm.DB, ids []uint) ([]models.Device, error)
|
||||
}
|
||||
|
||||
// gormDeviceRepository 是 DeviceRepository 的 GORM 实现
|
||||
@@ -66,6 +69,18 @@ func (r *gormDeviceRepository) FindByID(id uint) (*models.Device, error) {
|
||||
return &device, nil
|
||||
}
|
||||
|
||||
// GetDevicesByIDsTx 在指定事务中根据ID列表获取设备
|
||||
func (r *gormDeviceRepository) GetDevicesByIDsTx(tx *gorm.DB, ids []uint) ([]models.Device, error) {
|
||||
var devices []models.Device
|
||||
if len(ids) == 0 {
|
||||
return devices, nil
|
||||
}
|
||||
if err := tx.Where("id IN ?", ids).Find(&devices).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
// FindByIDString 根据字符串形式的主键 ID 查找设备
|
||||
func (r *gormDeviceRepository) FindByIDString(id string) (*models.Device, error) {
|
||||
// 将字符串ID转换为uint64
|
||||
|
||||
@@ -48,6 +48,8 @@ type PlanRepository interface {
|
||||
GetPlansByIDs(ids []uint) ([]models.Plan, error)
|
||||
// CreatePlan 创建一个新的计划
|
||||
CreatePlan(plan *models.Plan) error
|
||||
// CreatePlanTx 在指定事务中创建一个新的计划
|
||||
CreatePlanTx(tx *gorm.DB, plan *models.Plan) error
|
||||
// UpdatePlanMetadataAndStructure 更新计划的元数据和结构,但不包括状态等运行时信息
|
||||
UpdatePlanMetadataAndStructure(plan *models.Plan) error
|
||||
// UpdatePlan 更新计划的所有字段
|
||||
@@ -200,62 +202,66 @@ func (r *gormPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
|
||||
|
||||
// CreatePlan 创建一个新的计划
|
||||
func (r *gormPlanRepository) CreatePlan(plan *models.Plan) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 前置校验
|
||||
if plan.ID != 0 {
|
||||
return ErrCreateWithNonZeroID
|
||||
}
|
||||
return r.CreatePlanTx(r.db, plan)
|
||||
}
|
||||
|
||||
// 检查是否同时包含任务和子计划
|
||||
if len(plan.Tasks) > 0 && len(plan.SubPlans) > 0 {
|
||||
return ErrMixedContent
|
||||
}
|
||||
// CreatePlanTx 在指定事务中创建一个新的计划
|
||||
func (r *gormPlanRepository) CreatePlanTx(tx *gorm.DB, plan *models.Plan) error {
|
||||
// 1. 前置校验
|
||||
if plan.ID != 0 {
|
||||
return ErrCreateWithNonZeroID
|
||||
}
|
||||
|
||||
// 检查是否有重复的执行顺序
|
||||
if err := plan.ValidateExecutionOrder(); err != nil {
|
||||
return fmt.Errorf("计划 (ID: %d) 的执行顺序无效: %w", plan.ID, err)
|
||||
}
|
||||
// 检查是否同时包含任务和子计划
|
||||
if len(plan.Tasks) > 0 && len(plan.SubPlans) > 0 {
|
||||
return ErrMixedContent
|
||||
}
|
||||
|
||||
// 如果是子计划类型,验证所有子计划是否存在且ID不为0
|
||||
if plan.ContentType == models.PlanContentTypeSubPlans {
|
||||
childIDsToValidate := make(map[uint]bool)
|
||||
for _, subPlanLink := range plan.SubPlans {
|
||||
if subPlanLink.ChildPlanID == 0 {
|
||||
return ErrSubPlanIDIsZeroOnCreate
|
||||
}
|
||||
childIDsToValidate[subPlanLink.ChildPlanID] = true
|
||||
// 检查是否有重复的执行顺序
|
||||
if err := plan.ValidateExecutionOrder(); err != nil {
|
||||
return fmt.Errorf("计划 (ID: %d) 的执行顺序无效: %w", plan.ID, err)
|
||||
}
|
||||
|
||||
// 如果是子计划类型,验证所有子计划是否存在且ID不为0
|
||||
if plan.ContentType == models.PlanContentTypeSubPlans {
|
||||
childIDsToValidate := make(map[uint]bool)
|
||||
for _, subPlanLink := range plan.SubPlans {
|
||||
if subPlanLink.ChildPlanID == 0 {
|
||||
return ErrSubPlanIDIsZeroOnCreate
|
||||
}
|
||||
childIDsToValidate[subPlanLink.ChildPlanID] = true
|
||||
}
|
||||
|
||||
var ids []uint
|
||||
for id := range childIDsToValidate {
|
||||
ids = append(ids, id)
|
||||
var ids []uint
|
||||
for id := range childIDsToValidate {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
var count int64
|
||||
if err := tx.Model(&models.Plan{}).Where("id IN ?", ids).Count(&count).Error; err != nil {
|
||||
return fmt.Errorf("验证子计划存在性失败: %w", err)
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
var count int64
|
||||
if err := tx.Model(&models.Plan{}).Where("id IN ?", ids).Count(&count).Error; err != nil {
|
||||
return fmt.Errorf("验证子计划存在性失败: %w", err)
|
||||
}
|
||||
if int(count) != len(ids) {
|
||||
return ErrNodeDoesNotExist
|
||||
}
|
||||
if int(count) != len(ids) {
|
||||
return ErrNodeDoesNotExist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 创建根计划
|
||||
// GORM 会自动处理关联的 Tasks (如果 ContentType 是 tasks 且 Task.ID 为 0)
|
||||
if err := tx.Create(plan).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 2. 创建根计划
|
||||
// GORM 会自动处理关联的 Tasks (如果 ContentType 是 tasks 且 Task.ID 为 0),
|
||||
// 以及 Tasks 内部已经填充好的 Devices 关联。
|
||||
if err := tx.Create(plan).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 创建触发器Task
|
||||
// 关键修改:调用 createPlanAnalysisTask 并处理其返回的 Task 对象
|
||||
_, err := r.createPlanAnalysisTask(tx, plan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
// 3. 创建触发器Task
|
||||
// 关键修改:调用 createPlanAnalysisTask 并处理其返回的 Task 对象
|
||||
_, err := r.createPlanAnalysisTask(tx, plan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePlan 更新计划
|
||||
@@ -414,9 +420,7 @@ func (r *gormPlanRepository) reconcileTasks(tx *gorm.DB, plan *models.Plan) erro
|
||||
}
|
||||
|
||||
if len(tasksToDelete) > 0 {
|
||||
if err := tx.Delete(&models.Task{}, tasksToDelete).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return r.deleteTasksTx(tx, tasksToDelete)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -555,42 +559,43 @@ func (r *gormPlanRepository) flattenPlanTasksRecursive(plan *models.Plan) ([]mod
|
||||
func (r *gormPlanRepository) DeleteTask(id int) error {
|
||||
// 使用事务确保操作的原子性
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
return r.deleteTask(tx, id)
|
||||
return r.deleteTasksTx(tx, []int{id})
|
||||
})
|
||||
}
|
||||
|
||||
// deleteTask 根据ID删除任务
|
||||
func (r *gormPlanRepository) deleteTask(tx *gorm.DB, id int) error {
|
||||
// 1. 检查是否有待执行任务引用了这个任务
|
||||
// deleteTasksTx 在事务中批量软删除任务,并物理删除其在关联表中的记录
|
||||
func (r *gormPlanRepository) deleteTasksTx(tx *gorm.DB, ids []int) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否有待执行任务引用了这些任务
|
||||
var pendingTaskCount int64
|
||||
if err := tx.Model(&models.PendingTask{}).Where("task_id = ?", id).Count(&pendingTaskCount).Error; err != nil {
|
||||
if err := tx.Model(&models.PendingTask{}).Where("task_id IN ?", ids).Count(&pendingTaskCount).Error; err != nil {
|
||||
return fmt.Errorf("检查待执行任务时出错: %w", err)
|
||||
}
|
||||
|
||||
// 如果有待执行任务引用该任务,不能删除
|
||||
if pendingTaskCount > 0 {
|
||||
return fmt.Errorf("无法删除任务(ID: %d),因为存在 %d 条待执行任务引用该任务", id, pendingTaskCount)
|
||||
return fmt.Errorf("无法删除任务,因为存在 %d 条待执行任务引用这些任务", pendingTaskCount)
|
||||
}
|
||||
|
||||
// 2. 检查是否有计划仍在使用这个任务
|
||||
var planCount int64
|
||||
if err := tx.Model(&models.Plan{}).Joins("JOIN tasks ON plans.id = tasks.plan_id").Where("tasks.id = ?", id).Count(&planCount).Error; err != nil {
|
||||
return fmt.Errorf("检查计划引用任务时出错: %w", err)
|
||||
// 因为钩子函数在批量删除中不会被触发, 所以手动删除关联表, 通过批量删除语句优化性能
|
||||
|
||||
// 1. 直接、高效地从关联表中物理删除所有相关记录
|
||||
// 这是最关键的优化,避免了不必要的查询和循环
|
||||
if err := tx.Where("task_id IN ?", ids).Delete(&models.DeviceTask{}).Error; err != nil {
|
||||
return fmt.Errorf("清理任务的设备关联失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果有计划在使用该任务,不能删除
|
||||
if planCount > 0 {
|
||||
return fmt.Errorf("无法删除任务(ID: %d),因为存在 %d 个计划仍在使用该任务", id, planCount)
|
||||
}
|
||||
|
||||
// 3. 执行删除操作
|
||||
result := tx.Delete(&models.Task{}, id)
|
||||
// 2. 对任务本身进行软删除
|
||||
result := tx.Delete(&models.Task{}, ids)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("删除任务失败: %w", result.Error)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// 检查是否实际删除了记录
|
||||
if result.RowsAffected == 0 {
|
||||
// 3. 如果是单个删除且未找到记录,则返回错误
|
||||
if len(ids) == 1 && result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user